Refactor server API

This commit is contained in:
Alexander Nozik 2022-12-02 22:38:37 +03:00
parent d6c974fcbc
commit 20b20a621f
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
14 changed files with 381 additions and 376 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-4" version = "0.3.0-dev-5"
} }
subprojects { subprojects {

View File

@ -1,5 +1,8 @@
package space.kscience.visionforge.examples package space.kscience.visionforge.examples
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.routing.routing
import kotlinx.html.* import kotlinx.html.*
import space.kscience.dataforge.context.Global import space.kscience.dataforge.context.Global
import space.kscience.dataforge.context.fetch import space.kscience.dataforge.context.fetch
@ -9,13 +12,15 @@ import space.kscience.visionforge.html.formFragment
import space.kscience.visionforge.onPropertyChange import space.kscience.visionforge.onPropertyChange
import space.kscience.visionforge.server.close import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.serve import space.kscience.visionforge.server.visionPage
fun main() { fun main() {
val visionManager = Global.fetch(VisionManager) val visionManager = Global.fetch(VisionManager)
val server = visionManager.serve { val server = embeddedServer(CIO, 7777, "localhost") {
page(VisionPage.scriptHeader("js/visionforge-playground.js")) {
routing {
visionPage(visionManager, VisionPage.scriptHeader("js/visionforge-playground.js")) {
val form = formFragment("form") { val form = formFragment("form") {
label { label {
htmlFor = "fname" htmlFor = "fname"
@ -54,6 +59,7 @@ fun main() {
} }
} }
} }
}.start(false)
server.openInBrowser() server.openInBrowser()

View File

@ -6,6 +6,7 @@ import space.kscience.visionforge.html.ResourceLocation
import space.kscience.visionforge.html.VisionPage import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.importScriptHeader import space.kscience.visionforge.html.importScriptHeader
import space.kscience.visionforge.makeFile import space.kscience.visionforge.makeFile
import space.kscience.visionforge.visionManager
import java.awt.Desktop import java.awt.Desktop
import java.nio.file.Path import java.nio.file.Path
@ -16,7 +17,7 @@ public fun makeVisionFile(
show: Boolean = true, show: Boolean = true,
content: HtmlVisionFragment, content: HtmlVisionFragment,
): Unit { ): Unit {
val actualPath = VisionPage(Global, content = content).makeFile(path) { actualPath -> val actualPath = VisionPage(Global.visionManager, content = content).makeFile(path) { actualPath ->
mapOf( mapOf(
"title" to VisionPage.title(title), "title" to VisionPage.title(title),
"playground" to VisionPage.importScriptHeader("js/visionforge-playground.js", resourceLocation, actualPath), "playground" to VisionPage.importScriptHeader("js/visionforge-playground.js", resourceLocation, actualPath),

View File

@ -1,26 +1,28 @@
package ru.mipt.npm.sat package ru.mipt.npm.sat
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.http.content.resources
import io.ktor.server.http.content.static
import io.ktor.server.routing.routing
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.html.div import kotlinx.html.div
import kotlinx.html.h1 import kotlinx.html.h1
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.fetch import space.kscience.dataforge.context.fetch
import space.kscience.dataforge.meta.Null import space.kscience.dataforge.meta.Null
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Colors import space.kscience.visionforge.Colors
import space.kscience.visionforge.html.VisionPage import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.server.close import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.serve import space.kscience.visionforge.server.visionPage
import space.kscience.visionforge.solid.* import space.kscience.visionforge.solid.*
import space.kscience.visionforge.three.threeJsHeader import space.kscience.visionforge.three.threeJsHeader
import space.kscience.visionforge.visionManager
import kotlin.random.Random import kotlin.random.Random
@OptIn(DFExperimental::class)
fun main() { fun main() {
val satContext = Context("sat") { val satContext = Context("sat") {
plugin(Solids) plugin(Solids)
@ -35,14 +37,21 @@ fun main() {
} }
} }
val server = satContext.visionManager.serve { val server = embeddedServer(CIO,7777, "localhost") {
page(VisionPage.threeJsHeader, VisionPage.styleSheetHeader("css/styles.css")) { routing {
static {
resources()
}
visionPage(solids.visionManager, VisionPage.threeJsHeader, VisionPage.styleSheetHeader("css/styles.css")) {
div("flex-column") { div("flex-column") {
h1 { +"Satellite detector demo" } h1 { +"Satellite detector demo" }
vision { sat } vision { sat }
} }
} }
} }
}.start(false)
server.openInBrowser() server.openInBrowser()

View File

@ -1,6 +1,14 @@
package space.kscience.visionforge.jupyter package space.kscience.visionforge.jupyter
import io.ktor.http.URLProtocol
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.server.routing.Routing
import io.ktor.server.routing.route
import io.ktor.server.util.url
import io.ktor.server.websocket.WebSockets
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
@ -13,11 +21,14 @@ import space.kscience.dataforge.context.logger
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.meta.string import space.kscience.dataforge.meta.string
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.HtmlFormFragment import space.kscience.visionforge.html.HtmlFormFragment
import space.kscience.visionforge.html.HtmlVisionFragment import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.VisionCollector
import space.kscience.visionforge.html.visionFragment import space.kscience.visionforge.html.visionFragment
import space.kscience.visionforge.server.VisionServer import space.kscience.visionforge.server.VisionRouteConfiguration
import space.kscience.visionforge.server.serve import space.kscience.visionforge.server.require
import space.kscience.visionforge.server.serveVisionData
import space.kscience.visionforge.visionManager import space.kscience.visionforge.visionManager
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.random.Random import kotlin.random.Random
@ -34,10 +45,14 @@ internal fun TagConsumer<*>.renderScriptForId(id: String) {
* A handler class that includes a server and common utilities * A handler class that includes a server and common utilities
*/ */
public class VFForNotebook(override val context: Context) : ContextAware, CoroutineScope { public class VFForNotebook(override val context: Context) : ContextAware, CoroutineScope {
public val visionManager: VisionManager = context.visionManager
private val configuration = VisionRouteConfiguration(visionManager)
private var counter = 0 private var counter = 0
private var engine: ApplicationEngine? = null private var engine: ApplicationEngine? = null
private var server: VisionServer? = null
public var isolateFragments: Boolean = false public var isolateFragments: Boolean = false
@ -47,16 +62,15 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
isolateFragments = true isolateFragments = true
} }
public fun isServerRunning(): Boolean = server != null public fun isServerRunning(): Boolean = engine != null
public fun html(block: TagConsumer<*>.() -> Unit): MimeTypedResult = HTML(createHTML().apply(block).finalize()) public fun html(block: TagConsumer<*>.() -> Unit): MimeTypedResult = HTML(createHTML().apply(block).finalize())
public fun startServer( public fun startServer(
host: String = context.properties["visionforge.host"].string ?: "localhost", host: String = context.properties["visionforge.host"].string ?: "localhost",
port: Int = context.properties["visionforge.port"].int ?: VisionServer.DEFAULT_PORT, port: Int = context.properties["visionforge.port"].int ?: VisionRouteConfiguration.DEFAULT_PORT,
configuration: VisionServer.() -> Unit = {},
): MimeTypedResult = html { ): MimeTypedResult = html {
if (server != null) { if (engine != null) {
p { p {
style = "color: red;" style = "color: red;"
+"Stopping current VisionForge server" +"Stopping current VisionForge server"
@ -64,10 +78,9 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
} }
engine?.stop(1000, 2000) engine?.stop(1000, 2000)
engine = context.visionManager.serve(host, port) { engine = context.embeddedServer(CIO, port, host) {
configuration() install(WebSockets)
server = this }.start(false)
}.start()
p { p {
style = "color: blue;" style = "color: blue;"
@ -80,20 +93,46 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
logger.info { "Stopping VisionForge server" } logger.info { "Stopping VisionForge server" }
stop(1000, 2000) stop(1000, 2000)
engine = null engine = null
server = null
} }
} }
private fun produceHtmlString( private fun produceHtmlString(
fragment: HtmlVisionFragment, fragment: HtmlVisionFragment,
): String = createHTML().apply { ): String = createHTML().apply {
val server = server
val id = "fragment[${fragment.hashCode()}/${Random.nextUInt()}]" val id = "fragment[${fragment.hashCode()}/${Random.nextUInt()}]"
div { div {
this.id = id this.id = id
if (server != null) { val engine = engine
if (engine != null) {
//if server exist, serve dynamically //if server exist, serve dynamically
server.serveVisionsFromFragment(consumer, "content-${counter++}", fragment) //server.serveVisionsFromFragment(consumer, "content-${counter++}", fragment)
val cellRoute = "content-${counter++}"
val collector: VisionCollector = mutableMapOf()
val url = engine.environment.connectors.first().let {
url{
protocol = URLProtocol.WS
host = it.host
port = it.port
pathSegments = listOf(cellRoute, "ws")
}
}
visionFragment(
context,
embedData = true,
updatesUrl = url,
collector = collector,
fragment = fragment
)
engine.application.require(Routing) {
route(cellRoute) {
serveVisionData(TODO(), collector)
}
}
} else { } else {
//if not, use static rendering //if not, use static rendering
visionFragment(context, fragment = fragment) visionFragment(context, fragment = fragment)

View File

@ -48,7 +48,7 @@ public abstract class VFIntegrationBase(final override val context: Context) : J
render<Vision> { vision -> render<Vision> { vision ->
handler.produceHtml { handler.produceHtml {
vision { vision } vision(vision)
} }
} }
@ -83,7 +83,7 @@ public abstract class VFIntegrationBase(final override val context: Context) : J
} }
} }
fragment(fragment.formBody) fragment(fragment.formBody)
vision { fragment.vision } vision(fragment.vision)
} }
} }

View File

@ -2,10 +2,11 @@ package space.kscience.visionforge.html
import kotlinx.html.* import kotlinx.html.*
import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager import space.kscience.visionforge.VisionManager
@ -14,31 +15,48 @@ public typealias HtmlVisionFragment = VisionTagConsumer<*>.() -> Unit
@DFExperimental @DFExperimental
public fun HtmlVisionFragment(content: VisionTagConsumer<*>.() -> Unit): HtmlVisionFragment = content 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 * Render a fragment in the given consumer and return a map of extracted visions
* @param context a context used to create a vision fragment * @param context a context used to create a vision fragment
* @param embedData embed Vision initial state in the HTML * @param embedData embed Vision initial state in the HTML
* @param fetchDataUrl fetch data after first render from given url * @param fetchDataUrl fetch data after first render from given url
* @param fetchUpdatesUrl receive push updates from the server at given url * @param updatesUrl receive push updates from the server at given url
* @param idPrefix a prefix to be used before vision ids * @param idPrefix a prefix to be used before vision ids
* @param renderScript if true add rendering script after the fragment
*/ */
public fun TagConsumer<*>.visionFragment( public fun TagConsumer<*>.visionFragment(
context: Context = Global, context: Context,
embedData: Boolean = true, embedData: Boolean = true,
fetchDataUrl: String? = null, fetchDataUrl: String? = null,
fetchUpdatesUrl: String? = null, updatesUrl: String? = null,
idPrefix: String? = null, idPrefix: String? = null,
collector: VisionCollector = mutableMapOf(),
fragment: HtmlVisionFragment, fragment: HtmlVisionFragment,
): Map<Name, Vision> { ) {
val visionMap = HashMap<Name, Vision>()
val consumer = object : VisionTagConsumer<Any?>(this@visionFragment, context, idPrefix) { val consumer = object : VisionTagConsumer<Any?>(this@visionFragment, context, idPrefix) {
override fun <T> TagConsumer<T>.vision(name: Name?, buildOutput: VisionOutput.() -> Vision): T {
//Avoid re-creating cached visions
val actualName = name ?: NameToken(
DEFAULT_VISION_NAME,
buildOutput.hashCode().toUInt().toString()
).asName()
val (output, vision) = collector.getOrPut(actualName) {
val output = VisionOutput(context, actualName)
val vision = output.buildOutput()
output to vision
}
return addVision(actualName, output.visionManager, vision, output.meta)
}
override fun DIV.renderVision(manager: VisionManager, name: Name, vision: Vision, outputMeta: Meta) { override fun DIV.renderVision(manager: VisionManager, name: Name, vision: Vision, outputMeta: Meta) {
visionMap[name] = vision
// Toggle update mode // Toggle update mode
fetchUpdatesUrl?.let { updatesUrl?.let {
attributes[OUTPUT_CONNECT_ATTRIBUTE] = it attributes[OUTPUT_CONNECT_ATTRIBUTE] = it
} }
@ -59,22 +77,22 @@ public fun TagConsumer<*>.visionFragment(
} }
fragment(consumer) fragment(consumer)
return visionMap
} }
public fun FlowContent.visionFragment( public fun FlowContent.visionFragment(
context: Context = Global, context: Context,
embedData: Boolean = true, embedData: Boolean = true,
fetchDataUrl: String? = null, fetchDataUrl: String? = null,
flowDataUrl: String? = null, updatesUrl: String? = null,
idPrefix: String? = null, idPrefix: String? = null,
visionCache: VisionCollector = mutableMapOf(),
fragment: HtmlVisionFragment, fragment: HtmlVisionFragment,
): Map<Name, Vision> = consumer.visionFragment( ): Unit = consumer.visionFragment(
context, context,
embedData, embedData,
fetchDataUrl, fetchDataUrl,
flowDataUrl, updatesUrl,
idPrefix, idPrefix,
fragment visionCache,
fragment = fragment
) )

View File

@ -1,7 +1,7 @@
package space.kscience.visionforge.html package space.kscience.visionforge.html
import kotlinx.html.* import kotlinx.html.*
import space.kscience.dataforge.context.Context import space.kscience.visionforge.VisionManager
/** /**
* A structure representing a single page with Visions to be rendered. * A structure representing a single page with Visions to be rendered.
@ -9,7 +9,7 @@ import space.kscience.dataforge.context.Context
* @param pageHeaders static headers for this page. * @param pageHeaders static headers for this page.
*/ */
public data class VisionPage( public data class VisionPage(
public val context: Context, public val visionManager: VisionManager,
public val pageHeaders: Map<String, HtmlFragment> = emptyMap(), public val pageHeaders: Map<String, HtmlFragment> = emptyMap(),
public val content: HtmlVisionFragment, public val content: HtmlVisionFragment,
) { ) {

View File

@ -7,7 +7,6 @@ import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.MutableMeta import space.kscience.dataforge.meta.MutableMeta
import space.kscience.dataforge.meta.isEmpty import space.kscience.dataforge.meta.isEmpty
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
@ -25,9 +24,8 @@ public annotation class VisionDSL
/** /**
* A placeholder object to attach inline vision builders. * A placeholder object to attach inline vision builders.
*/ */
@DFExperimental
@VisionDSL @VisionDSL
public class VisionOutput @PublishedApi internal constructor(public val context: Context, public val name: Name?) { public class VisionOutput @PublishedApi internal constructor(public val context: Context, public val name: Name) {
public var meta: Meta = Meta.EMPTY public var meta: Meta = Meta.EMPTY
private val requirements: MutableSet<PluginFactory<*>> = HashSet() private val requirements: MutableSet<PluginFactory<*>> = HashSet()
@ -36,8 +34,8 @@ public class VisionOutput @PublishedApi internal constructor(public val context:
requirements.add(factory) requirements.add(factory)
} }
internal fun buildVisionManager(): VisionManager = public val visionManager: VisionManager
if (requirements.all { req -> context.plugins.find(true) { it.tag == req.tag } != null }) { get() = if (requirements.all { req -> context.plugins.find(true) { it.tag == req.tag } != null }) {
context.visionManager context.visionManager
} else { } else {
val newContext = context.buildContext(NameToken(DEFAULT_VISION_NAME, name.toString()).asName()) { val newContext = context.buildContext(NameToken(DEFAULT_VISION_NAME, name.toString()).asName()) {
@ -56,7 +54,6 @@ public class VisionOutput @PublishedApi internal constructor(public val context:
* Modified [TagConsumer] that allows rendering output fragments and visions in them * Modified [TagConsumer] that allows rendering output fragments and visions in them
*/ */
@VisionDSL @VisionDSL
@OptIn(DFExperimental::class)
public abstract class VisionTagConsumer<R>( public abstract class VisionTagConsumer<R>(
private val root: TagConsumer<R>, private val root: TagConsumer<R>,
public val context: Context, public val context: Context,
@ -78,12 +75,14 @@ public abstract class VisionTagConsumer<R>(
* Create a placeholder for a vision output with optional [Vision] in it * Create a placeholder for a vision output with optional [Vision] in it
* TODO with multi-receivers could be replaced by [VisionTagConsumer, TagConsumer] extension * TODO with multi-receivers could be replaced by [VisionTagConsumer, TagConsumer] extension
*/ */
private fun <T> TagConsumer<T>.vision( protected fun <T> TagConsumer<T>.addVision(
name: Name, name: Name,
manager: VisionManager, manager: VisionManager,
vision: Vision, vision: Vision?,
outputMeta: Meta = Meta.EMPTY, outputMeta: Meta = Meta.EMPTY,
): T = div { ): T = if (vision == null) div {
+"Empty Vision output"
} else div {
id = resolveId(name) id = resolveId(name)
classes = setOf(OUTPUT_CLASS) classes = setOf(OUTPUT_CLASS)
if (vision.parent == null) { if (vision.parent == null) {
@ -106,26 +105,35 @@ public abstract class VisionTagConsumer<R>(
* Insert a vision in this HTML. * Insert a vision in this HTML.
* TODO replace by multi-receiver * TODO replace by multi-receiver
*/ */
@OptIn(DFExperimental::class) @VisionDSL
public fun <T> TagConsumer<T>.vision( public open fun <T> TagConsumer<T>.vision(
name: Name? = null, name: Name? = null,
@OptIn(DFExperimental::class) visionProvider: VisionOutput.() -> Vision, buildOutput: VisionOutput.() -> Vision,
): T { ): T {
val output = VisionOutput(context, name) val actualName = name ?: NameToken(DEFAULT_VISION_NAME, buildOutput.hashCode().toUInt().toString()).asName()
val vision = output.visionProvider() val output = VisionOutput(context, actualName)
val actualName = name ?: NameToken(DEFAULT_VISION_NAME, vision.hashCode().toUInt().toString()).asName() val vision = output.buildOutput()
return vision(actualName, output.buildVisionManager(), vision, output.meta) return addVision(actualName, output.visionManager, vision, output.meta)
} }
/** /**
* TODO to be replaced by multi-receiver * TODO to be replaced by multi-receiver
*/ */
@OptIn(DFExperimental::class)
@VisionDSL @VisionDSL
public fun <T> TagConsumer<T>.vision( public fun <T> TagConsumer<T>.vision(
name: String?, name: String?,
@OptIn(DFExperimental::class) visionProvider: VisionOutput.() -> Vision, buildOutput: VisionOutput.() -> Vision,
): T = vision(name?.parseAsName(), visionProvider) ): T = vision(name?.parseAsName(), buildOutput)
@VisionDSL
public open fun <T> TagConsumer<T>.vision(
vision: Vision,
name: Name? = null,
outputMeta: Meta = Meta.EMPTY,
) {
val actualName = name ?: NameToken(DEFAULT_VISION_NAME, vision.hashCode().toUInt().toString()).asName()
addVision(actualName, context.visionManager, vision, outputMeta)
}
/** /**
* Process the resulting object produced by [TagConsumer] * Process the resulting object produced by [TagConsumer]

View File

@ -5,13 +5,13 @@ import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.MetaSerializer import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.isEmpty import space.kscience.dataforge.meta.isEmpty
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName import space.kscience.dataforge.names.asName
import space.kscience.dataforge.names.parseAsName import space.kscience.dataforge.names.parseAsName
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.VisionTagConsumer.Companion.DEFAULT_VISION_NAME
import space.kscience.visionforge.setAsRoot import space.kscience.visionforge.setAsRoot
import space.kscience.visionforge.visionManager import space.kscience.visionforge.visionManager
@ -34,11 +34,13 @@ public interface HtmlVisionContext : ContextAware {
public typealias HtmlVisionContextFragment = context(HtmlVisionContext) TagConsumer<*>.() -> Unit public typealias HtmlVisionContextFragment = context(HtmlVisionContext) TagConsumer<*>.() -> Unit
context(HtmlVisionContext) public fun HtmlVisionFragment( context(HtmlVisionContext)
content: TagConsumer<*>.() -> Unit public fun HtmlVisionFragment(
content: TagConsumer<*>.() -> Unit,
): HtmlVisionFragment = content ): HtmlVisionFragment = content
context(HtmlVisionContext) private fun <T> TagConsumer<T>.vision( context(HtmlVisionContext)
private fun <T> TagConsumer<T>.vision(
visionManager: VisionManager, visionManager: VisionManager,
name: Name, name: Name,
vision: Vision, vision: Vision,
@ -61,7 +63,7 @@ context(HtmlVisionContext) private fun <T> TagConsumer<T>.vision(
} }
context(HtmlVisionContext) context(HtmlVisionContext)
private fun <T> TagConsumer<T>.vision( private fun <T> TagConsumer<T>.vision(
name: Name, name: Name,
vision: Vision, vision: Vision,
outputMeta: Meta = Meta.EMPTY, outputMeta: Meta = Meta.EMPTY,
@ -71,26 +73,23 @@ context(HtmlVisionContext)
* Insert a vision in this HTML. * Insert a vision in this HTML.
*/ */
context(HtmlVisionContext) context(HtmlVisionContext)
@DFExperimental @VisionDSL
@VisionDSL public fun <T> TagConsumer<T>.vision(
public fun <T> TagConsumer<T>.vision(
name: Name? = null, name: Name? = null,
visionProvider: VisionOutput.() -> Vision, visionProvider: VisionOutput.() -> Vision,
): T { ): T {
val output = VisionOutput(context, name) val actualName = name ?: NameToken(DEFAULT_VISION_NAME, visionProvider.hashCode().toUInt().toString()).asName()
val output = VisionOutput(context, actualName)
val vision = output.visionProvider() val vision = output.visionProvider()
val actualName = return vision(output.visionManager, actualName, vision, output.meta)
name ?: NameToken(VisionTagConsumer.DEFAULT_VISION_NAME, vision.hashCode().toUInt().toString()).asName()
return vision(output.buildVisionManager(), actualName, vision, output.meta)
} }
/** /**
* Insert a vision in this HTML. * Insert a vision in this HTML.
*/ */
context(HtmlVisionContext) context(HtmlVisionContext)
@DFExperimental @VisionDSL
@VisionDSL public fun <T> TagConsumer<T>.vision(
public fun <T> TagConsumer<T>.vision(
name: String?, name: String?,
visionProvider: VisionOutput.() -> Vision, visionProvider: VisionOutput.() -> Vision,
): T = vision(name?.parseAsName(), visionProvider) ): T = vision(name?.parseAsName(), visionProvider)

View File

@ -4,6 +4,7 @@ import kotlinx.html.body
import kotlinx.html.head import kotlinx.html.head
import kotlinx.html.meta import kotlinx.html.meta
import kotlinx.html.stream.createHTML import kotlinx.html.stream.createHTML
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.html.HtmlFragment import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.VisionPage import space.kscience.visionforge.html.VisionPage
@ -86,7 +87,7 @@ public fun VisionPage.makeFile(
} }
} }
body { body {
visionFragment(context, fragment = content) visionFragment(Global, fragment = content)
} }
}.finalize() }.finalize()

View File

@ -2,39 +2,34 @@ package space.kscience.visionforge.server
import io.ktor.http.* import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.cio.CIO import io.ktor.server.cio.*
import io.ktor.server.engine.ApplicationEngine import io.ktor.server.engine.*
import io.ktor.server.engine.embeddedServer import io.ktor.server.html.*
import io.ktor.server.html.respondHtml import io.ktor.server.http.content.*
import io.ktor.server.http.content.resources import io.ktor.server.plugins.*
import io.ktor.server.http.content.static import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.plugins.cors.routing.CORS import io.ktor.server.request.*
import io.ktor.server.response.respond import io.ktor.server.response.*
import io.ktor.server.response.respondText
import io.ktor.server.routing.* import io.ktor.server.routing.*
import io.ktor.server.util.getOrFail import io.ktor.server.util.*
import io.ktor.server.websocket.WebSockets import io.ktor.server.websocket.*
import io.ktor.server.websocket.webSocket import io.ktor.util.pipeline.*
import io.ktor.util.pipeline.Pipeline import io.ktor.websocket.*
import io.ktor.websocket.Frame
import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.channels.consumeEach
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
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.stream.createHTML import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.meta.* import space.kscience.dataforge.meta.*
import space.kscience.dataforge.names.Name import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionChange import space.kscience.visionforge.VisionChange
import space.kscience.visionforge.VisionManager import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.flowChanges import space.kscience.visionforge.flowChanges
import space.kscience.visionforge.html.HtmlFragment import space.kscience.visionforge.html.*
import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.visionFragment
import space.kscience.visionforge.server.VisionServer.Companion.DEFAULT_PAGE
import java.awt.Desktop import java.awt.Desktop
import java.net.URI import java.net.URI
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -57,210 +52,20 @@ public enum class DataServeMode {
UPDATE UPDATE
} }
/** public class VisionRouteConfiguration(
* A ktor plugin container with given [routing] public val visionManager: VisionManager,
* @param serverUrl a server url including root route override val meta: ObservableMutableMeta = MutableMeta(),
*/ ) : Configurable, ContextAware {
public class VisionServer internal constructor(
private val visionManager: VisionManager,
private val serverUrl: Url,
private val root: Route,
) : Configurable {
public val application: Application get() = root.application override val context: Context get() = visionManager.context
override val meta: ObservableMutableMeta = MutableMeta()
/** /**
* Update minimal interval between updates in milliseconds (if there are no updates, push will not happen * Update minimal interval between updates in milliseconds (if there are no updates, push will not happen
*/ */
public var updateInterval: Long by meta.long(300, key = UPDATE_INTERVAL_KEY) public var updateInterval: Long by meta.long(300, key = UPDATE_INTERVAL_KEY)
/**
* Cache page fragments. If false, pages will be reconstructed on each call. Default: `true`
*/
public var cacheFragments: Boolean by meta.boolean(true)
public var dataMode: DataServeMode by meta.enum(DataServeMode.UPDATE) public var dataMode: DataServeMode by meta.enum(DataServeMode.UPDATE)
private val serverHeaders: MutableMap<String, HtmlFragment> = mutableMapOf()
/**
* Set up a default header that is automatically added to all pages on this server
*/
public fun header(key: String, block: HtmlFragment) {
serverHeaders[key] = block
}
private fun HTML.visionPage(
pagePath: String,
headers: Map<String, HtmlFragment>,
visionFragment: HtmlVisionFragment,
): Map<Name, Vision> {
var visionMap: Map<Name, Vision>? = null
head {
meta {
charset = "utf-8"
}
(serverHeaders + headers).values.forEach {
consumer.it()
}
}
body {
//Load the fragment and remember all loaded visions
visionMap = visionFragment(
context = visionManager.context,
embedData = dataMode == DataServeMode.EMBED,
fetchDataUrl = if (dataMode != DataServeMode.EMBED) "$serverUrl$pagePath/data" else null,
flowDataUrl = if (dataMode == DataServeMode.UPDATE) "$serverUrl$pagePath/ws" else null,
fragment = visionFragment
)
}
return visionMap!!
}
/**
* Server a map of visions without providing explicit html page for them
*/
private fun serveVisions(route: Route, visions: Map<Name, Vision>): Unit = route {
application.log.info("Serving visions $visions at $route")
//Update websocket
webSocket("ws") {
val name: String = call.request.queryParameters.getOrFail("name")
application.log.debug("Opened server socket for $name")
val vision: Vision = visions[Name.parse(name)] ?: error("Plot with id='$name' not registered")
launch {
incoming.consumeEach {
val data = it.data.decodeToString()
application.log.debug("Received update: \n$data")
val change = visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(), data
)
vision.update(change)
}
}
try {
withContext(visionManager.context.coroutineContext) {
vision.flowChanges(updateInterval.milliseconds).onEach { update ->
val json = visionManager.jsonFormat.encodeToString(
VisionChange.serializer(),
update
)
application.log.debug("Sending update: \n$json")
outgoing.send(Frame.Text(json))
}.collect()
}
} catch (t: Throwable) {
application.log.info("WebSocket update channel for $name is closed with exception: $t")
}
}
//Plots in their json representation
get("data") {
val name: String = call.request.queryParameters.getOrFail("name")
val vision: Vision? = visions[Name.parse(name)]
if (vision == null) {
call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found")
} else {
call.respondText(
visionManager.encodeToString(vision),
contentType = ContentType.Application.Json,
status = HttpStatusCode.OK
)
}
}
}
/**
* Serve visions in a given [route] without providing a page template
*/
public fun serveVisions(route: String, visions: Map<Name, Vision>) {
root.route(route) {
serveVisions(this, visions)
}
}
/**
* Compile a fragment to string and serve visions from it
*/
public fun serveVisionsFromFragment(
consumer: TagConsumer<*>,
route: String,
fragment: HtmlVisionFragment,
): Unit {
val visions = consumer.visionFragment(
visionManager.context,
embedData = true,
fetchUpdatesUrl = "$serverUrl$route/ws",
fragment = fragment
)
serveVisions(route, visions)
}
/**
* Serve a page, potentially containing any number of visions at a given [route] with given [header].
*/
public fun page(
route: String = DEFAULT_PAGE,
headers: Map<String, HtmlFragment>,
visionFragment: HtmlVisionFragment,
) {
val visions = HashMap<Name, Vision>()
val cachedHtml: String? = if (cacheFragments) {
//Create and cache page html and map of visions
createHTML(true).html {
visions.putAll(visionPage(route, headers, visionFragment))
}
} else {
null
}
root.route(route) {
serveVisions(this, visions)
//filled pages
get {
if (cachedHtml == null) {
//re-create html and vision list on each call
call.respondHtml {
visions.clear()
visions.putAll(visionPage(route, headers, visionFragment))
}
} else {
//Use cached html
call.respondText(cachedHtml, ContentType.Text.Html.withCharset(Charsets.UTF_8))
}
}
}
}
public fun page(
vararg headers: HtmlFragment,
route: String = DEFAULT_PAGE,
title: String = "VisionForge server page '$route'",
visionFragment: HtmlVisionFragment,
) {
page(
route,
mapOf("title" to VisionPage.title(title)) + headers.associateBy { it.hashCode().toString() },
visionFragment
)
}
/**
* Render given [VisionPage] at server
*/
public fun page(route: String, page: VisionPage) {
page(route = route, headers = page.pageHeaders, visionFragment = page.content)
}
public companion object { public companion object {
public const val DEFAULT_PORT: Int = 7777 public const val DEFAULT_PORT: Int = 7777
public const val DEFAULT_PAGE: String = "/" public const val DEFAULT_PAGE: String = "/"
@ -268,50 +73,168 @@ public class VisionServer internal constructor(
} }
} }
/**
* Serve visions in a given [route] without providing a page template.
* [visions] could be changed during the service.
*/
public fun Route.serveVisionData(
configuration: VisionRouteConfiguration,
resolveVision: (Name) -> Vision?,
) {
application.log.info("Serving visions at ${this@serveVisionData}")
//Update websocket
webSocket("ws") {
val name: String = call.request.queryParameters.getOrFail("name")
application.log.debug("Opened server socket for $name")
val vision: Vision = resolveVision(Name.parse(name)) ?: error("Plot with id='$name' not registered")
launch {
incoming.consumeEach {
val data = it.data.decodeToString()
application.log.debug("Received update: \n$data")
val change = configuration.visionManager.jsonFormat.decodeFromString(
VisionChange.serializer(), data
)
vision.update(change)
}
}
try {
withContext(configuration.context.coroutineContext) {
vision.flowChanges(configuration.updateInterval.milliseconds).onEach { update ->
val json = configuration.visionManager.jsonFormat.encodeToString(
VisionChange.serializer(),
update
)
application.log.debug("Sending update: \n$json")
outgoing.send(Frame.Text(json))
}.collect()
}
} catch (t: Throwable) {
this.application.log.info("WebSocket update channel for $name is closed with exception: $t")
}
}
//Plots in their json representation
get("data") {
val name: String = call.request.queryParameters.getOrFail("name")
val vision: Vision? = resolveVision(Name.parse(name))
if (vision == null) {
call.respond(HttpStatusCode.NotFound, "Vision with name '$name' not found")
} else {
call.respondText(
configuration.visionManager.encodeToString(vision),
contentType = ContentType.Application.Json,
status = HttpStatusCode.OK
)
}
}
}
public fun Route.serveVisionData(
configuration: VisionRouteConfiguration,
cache: VisionCollector,
): Unit = serveVisionData(configuration) { cache[it]?.second }
//
///**
// * Compile a fragment to string and serve visions from it
// */
//public fun Route.serveVisionsFromFragment(
// consumer: TagConsumer<*>,
// sererPageUrl: Url,
// visionManager: VisionManager,
// fragment: HtmlVisionFragment,
//): Unit {
// val visions = consumer.visionFragment(
// visionManager.context,
// embedData = true,
// fetchUpdatesUrl = "$serverUrl$route/ws",
// fragment = fragment
// )
//
// serveVisionData(visionManager, visions)
//}
/**
* Serve a page, potentially containing any number of visions at a given [route] with given [header].
*/
public fun Route.visionPage(
configuration: VisionRouteConfiguration,
headers: Collection<HtmlFragment>,
visionFragment: HtmlVisionFragment,
) {
application.require(WebSockets)
require(CORS) {
anyHost()
}
val visionCache: VisionCollector = mutableMapOf()
serveVisionData(configuration, visionCache)
//filled pages
get {
//re-create html and vision list on each call
call.respondHtml {
val callbackUrl = call.url()
head {
meta {
charset = "utf-8"
}
headers.forEach { header ->
consumer.header()
}
}
body {
//Load the fragment and remember all loaded visions
visionFragment(
context = configuration.context,
embedData = configuration.dataMode == DataServeMode.EMBED,
fetchDataUrl = if (configuration.dataMode != DataServeMode.EMBED) {
URLBuilder(callbackUrl).apply {
pathSegments = pathSegments + "data"
}.buildString()
} else null,
updatesUrl = if (configuration.dataMode == DataServeMode.UPDATE) {
URLBuilder(callbackUrl).apply {
protocol = URLProtocol.WS
pathSegments = pathSegments + "ws"
}.buildString()
} else null,
visionCache = visionCache,
fragment = visionFragment
)
}
}
}
}
public fun Route.visionPage(
visionManager: VisionManager,
vararg headers: HtmlFragment,
configurationBuilder: VisionRouteConfiguration.() -> Unit = {},
visionFragment: HtmlVisionFragment,
) {
val configuration = VisionRouteConfiguration(visionManager).apply(configurationBuilder)
visionPage(configuration, listOf(*headers), visionFragment)
}
/**
* Render given [VisionPage] at server
*/
public fun Route.visionPage(page: VisionPage, configurationBuilder: VisionRouteConfiguration.() -> Unit = {}) {
val configuration = VisionRouteConfiguration(page.visionManager).apply(configurationBuilder)
visionPage(configuration, page.pageHeaders.values, visionFragment = page.content)
}
public fun <P : Pipeline<*, ApplicationCall>, B : Any, F : Any> P.require( public fun <P : Pipeline<*, ApplicationCall>, B : Any, F : Any> P.require(
plugin: Plugin<P, B, F>, plugin: Plugin<P, B, F>,
configure: B.() -> Unit = {}, configure: B.() -> Unit = {},
): F = pluginOrNull(plugin) ?: install(plugin, configure) ): F = pluginOrNull(plugin) ?: install(plugin, configure)
/**
* Attach VisionForge server application to given server
*/
public fun Application.visionServer(
visionManager: VisionManager,
webServerUrl: Url,
path: String = DEFAULT_PAGE,
): VisionServer {
require(WebSockets)
require(CORS) {
anyHost()
}
// if (pluginOrNull(CallLogging) == null) {
// install(CallLogging)
// }
val serverRoute = require(Routing).createRouteFromPath(path)
serverRoute {
static {
resources()
}
}
return VisionServer(visionManager, URLBuilder(webServerUrl).apply { encodedPath = path }.build(), serverRoute)
}
/**
* Start a stand-alone VisionForge server at given host/port
*/
public fun VisionManager.serve(
host: String = "localhost",
port: Int = VisionServer.DEFAULT_PORT,
block: VisionServer.() -> Unit,
): ApplicationEngine = context.embeddedServer(CIO, port, host) {
val url = URLBuilder(host = host, port = port).build()
visionServer(this@serve, url).apply(block)
}.start()
/** /**
* Connect to a given Ktor server using browser * Connect to a given Ktor server using browser

View File

@ -33,7 +33,7 @@ public class Solids(meta: Meta) : VisionPlugin(meta), MutableVisionContainer<Sol
override val type: KClass<out Solids> = Solids::class override val type: KClass<out Solids> = Solids::class
public val default: Solids by lazy { public val default: Solids by lazy {
Context("@Solids"){ Context("@Solids") {
plugin(Solids) plugin(Solids)
}.fetch(Solids) }.fetch(Solids)
} }

View File

@ -4,6 +4,7 @@ import space.kscience.dataforge.context.Global
import space.kscience.dataforge.misc.DFExperimental import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.html.* import space.kscience.visionforge.html.*
import space.kscience.visionforge.makeFile import space.kscience.visionforge.makeFile
import space.kscience.visionforge.visionManager
import java.awt.Desktop import java.awt.Desktop
import java.nio.file.Path import java.nio.file.Path
@ -19,7 +20,7 @@ public fun makeThreeJsFile(
show: Boolean = true, show: Boolean = true,
content: HtmlVisionFragment, content: HtmlVisionFragment,
): Unit { ): Unit {
val actualPath = VisionPage(Global, content = content).makeFile(path) { actualPath -> val actualPath = VisionPage(Global.visionManager, content = content).makeFile(path) { actualPath ->
mapOf( mapOf(
"title" to VisionPage.title(title), "title" to VisionPage.title(title),
"threeJs" to VisionPage.importScriptHeader("js/visionforge-three.js", resourceLocation, actualPath) "threeJs" to VisionPage.importScriptHeader("js/visionforge-three.js", resourceLocation, actualPath)