Refactor server API
This commit is contained in:
parent
d6c974fcbc
commit
20b20a621f
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
@ -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]
|
||||||
|
@ -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,
|
||||||
@ -71,24 +73,21 @@ 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?,
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user