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

View File

@ -1,5 +1,8 @@
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 space.kscience.dataforge.context.Global
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.server.close
import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.serve
import space.kscience.visionforge.server.visionPage
fun main() {
val visionManager = Global.fetch(VisionManager)
val server = visionManager.serve {
page(VisionPage.scriptHeader("js/visionforge-playground.js")) {
val server = embeddedServer(CIO, 7777, "localhost") {
routing {
visionPage(visionManager, VisionPage.scriptHeader("js/visionforge-playground.js")) {
val form = formFragment("form") {
label {
htmlFor = "fname"
@ -54,6 +59,7 @@ fun main() {
}
}
}
}.start(false)
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.importScriptHeader
import space.kscience.visionforge.makeFile
import space.kscience.visionforge.visionManager
import java.awt.Desktop
import java.nio.file.Path
@ -16,7 +17,7 @@ public fun makeVisionFile(
show: Boolean = true,
content: HtmlVisionFragment,
): Unit {
val actualPath = VisionPage(Global, content = content).makeFile(path) { actualPath ->
val actualPath = VisionPage(Global.visionManager, content = content).makeFile(path) { actualPath ->
mapOf(
"title" to VisionPage.title(title),
"playground" to VisionPage.importScriptHeader("js/visionforge-playground.js", resourceLocation, actualPath),

View File

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

View File

@ -1,6 +1,14 @@
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.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.html.*
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.int
import space.kscience.dataforge.meta.string
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.HtmlFormFragment
import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.VisionCollector
import space.kscience.visionforge.html.visionFragment
import space.kscience.visionforge.server.VisionServer
import space.kscience.visionforge.server.serve
import space.kscience.visionforge.server.VisionRouteConfiguration
import space.kscience.visionforge.server.require
import space.kscience.visionforge.server.serveVisionData
import space.kscience.visionforge.visionManager
import kotlin.coroutines.CoroutineContext
import kotlin.random.Random
@ -34,10 +45,14 @@ internal fun TagConsumer<*>.renderScriptForId(id: String) {
* A handler class that includes a server and common utilities
*/
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 engine: ApplicationEngine? = null
private var server: VisionServer? = null
public var isolateFragments: Boolean = false
@ -47,16 +62,15 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
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 startServer(
host: String = context.properties["visionforge.host"].string ?: "localhost",
port: Int = context.properties["visionforge.port"].int ?: VisionServer.DEFAULT_PORT,
configuration: VisionServer.() -> Unit = {},
port: Int = context.properties["visionforge.port"].int ?: VisionRouteConfiguration.DEFAULT_PORT,
): MimeTypedResult = html {
if (server != null) {
if (engine != null) {
p {
style = "color: red;"
+"Stopping current VisionForge server"
@ -64,10 +78,9 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
}
engine?.stop(1000, 2000)
engine = context.visionManager.serve(host, port) {
configuration()
server = this
}.start()
engine = context.embeddedServer(CIO, port, host) {
install(WebSockets)
}.start(false)
p {
style = "color: blue;"
@ -80,20 +93,46 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
logger.info { "Stopping VisionForge server" }
stop(1000, 2000)
engine = null
server = null
}
}
private fun produceHtmlString(
fragment: HtmlVisionFragment,
): String = createHTML().apply {
val server = server
val id = "fragment[${fragment.hashCode()}/${Random.nextUInt()}]"
div {
this.id = id
if (server != null) {
val engine = engine
if (engine != null) {
//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 {
//if not, use static rendering
visionFragment(context, fragment = fragment)

View File

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

View File

@ -2,10 +2,11 @@ package space.kscience.visionforge.html
import kotlinx.html.*
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.Global
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.misc.DFExperimental
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.VisionManager
@ -14,31 +15,48 @@ public typealias HtmlVisionFragment = VisionTagConsumer<*>.() -> Unit
@DFExperimental
public fun HtmlVisionFragment(content: VisionTagConsumer<*>.() -> Unit): HtmlVisionFragment = content
public typealias VisionCollector = MutableMap<Name, Pair<VisionOutput, Vision>>
/**
* Render a fragment in the given consumer and return a map of extracted visions
* @param context a context used to create a vision fragment
* @param embedData embed Vision initial state in the HTML
* @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 renderScript if true add rendering script after the fragment
*/
public fun TagConsumer<*>.visionFragment(
context: Context = Global,
context: Context,
embedData: Boolean = true,
fetchDataUrl: String? = null,
fetchUpdatesUrl: String? = null,
updatesUrl: String? = null,
idPrefix: String? = null,
collector: VisionCollector = mutableMapOf(),
fragment: HtmlVisionFragment,
): Map<Name, Vision> {
val visionMap = HashMap<Name, Vision>()
) {
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) {
visionMap[name] = vision
// Toggle update mode
fetchUpdatesUrl?.let {
updatesUrl?.let {
attributes[OUTPUT_CONNECT_ATTRIBUTE] = it
}
@ -59,22 +77,22 @@ public fun TagConsumer<*>.visionFragment(
}
fragment(consumer)
return visionMap
}
public fun FlowContent.visionFragment(
context: Context = Global,
context: Context,
embedData: Boolean = true,
fetchDataUrl: String? = null,
flowDataUrl: String? = null,
updatesUrl: String? = null,
idPrefix: String? = null,
visionCache: VisionCollector = mutableMapOf(),
fragment: HtmlVisionFragment,
): Map<Name, Vision> = consumer.visionFragment(
): Unit = consumer.visionFragment(
context,
embedData,
fetchDataUrl,
flowDataUrl,
updatesUrl,
idPrefix,
fragment
visionCache,
fragment = fragment
)

View File

@ -1,7 +1,7 @@
package space.kscience.visionforge.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.
@ -9,7 +9,7 @@ import space.kscience.dataforge.context.Context
* @param pageHeaders static headers for this page.
*/
public data class VisionPage(
public val context: Context,
public val visionManager: VisionManager,
public val pageHeaders: Map<String, HtmlFragment> = emptyMap(),
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.MutableMeta
import space.kscience.dataforge.meta.isEmpty
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.NameToken
import space.kscience.dataforge.names.asName
@ -25,9 +24,8 @@ public annotation class VisionDSL
/**
* A placeholder object to attach inline vision builders.
*/
@DFExperimental
@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
private val requirements: MutableSet<PluginFactory<*>> = HashSet()
@ -36,8 +34,8 @@ public class VisionOutput @PublishedApi internal constructor(public val context:
requirements.add(factory)
}
internal fun buildVisionManager(): VisionManager =
if (requirements.all { req -> context.plugins.find(true) { it.tag == req.tag } != null }) {
public val visionManager: VisionManager
get() = if (requirements.all { req -> context.plugins.find(true) { it.tag == req.tag } != null }) {
context.visionManager
} else {
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
*/
@VisionDSL
@OptIn(DFExperimental::class)
public abstract class VisionTagConsumer<R>(
private val root: TagConsumer<R>,
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
* 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,
manager: VisionManager,
vision: Vision,
vision: Vision?,
outputMeta: Meta = Meta.EMPTY,
): T = div {
): T = if (vision == null) div {
+"Empty Vision output"
} else div {
id = resolveId(name)
classes = setOf(OUTPUT_CLASS)
if (vision.parent == null) {
@ -106,26 +105,35 @@ public abstract class VisionTagConsumer<R>(
* Insert a vision in this HTML.
* TODO replace by multi-receiver
*/
@OptIn(DFExperimental::class)
public fun <T> TagConsumer<T>.vision(
@VisionDSL
public open fun <T> TagConsumer<T>.vision(
name: Name? = null,
@OptIn(DFExperimental::class) visionProvider: VisionOutput.() -> Vision,
buildOutput: VisionOutput.() -> Vision,
): T {
val output = VisionOutput(context, name)
val vision = output.visionProvider()
val actualName = name ?: NameToken(DEFAULT_VISION_NAME, vision.hashCode().toUInt().toString()).asName()
return vision(actualName, output.buildVisionManager(), vision, output.meta)
val actualName = name ?: NameToken(DEFAULT_VISION_NAME, buildOutput.hashCode().toUInt().toString()).asName()
val output = VisionOutput(context, actualName)
val vision = output.buildOutput()
return addVision(actualName, output.visionManager, vision, output.meta)
}
/**
* TODO to be replaced by multi-receiver
*/
@OptIn(DFExperimental::class)
@VisionDSL
public fun <T> TagConsumer<T>.vision(
name: String?,
@OptIn(DFExperimental::class) visionProvider: VisionOutput.() -> Vision,
): T = vision(name?.parseAsName(), visionProvider)
buildOutput: VisionOutput.() -> Vision,
): 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]

View File

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

View File

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

View File

@ -2,39 +2,34 @@ package space.kscience.visionforge.server
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.cio.CIO
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.embeddedServer
import io.ktor.server.html.respondHtml
import io.ktor.server.http.content.resources
import io.ktor.server.http.content.static
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.cio.*
import io.ktor.server.engine.*
import io.ktor.server.html.*
import io.ktor.server.http.content.*
import io.ktor.server.plugins.*
import io.ktor.server.plugins.cors.routing.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.util.getOrFail
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.webSocket
import io.ktor.util.pipeline.Pipeline
import io.ktor.websocket.Frame
import io.ktor.server.util.*
import io.ktor.server.websocket.*
import io.ktor.util.pipeline.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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.names.Name
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionChange
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.flowChanges
import space.kscience.visionforge.html.HtmlFragment
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 space.kscience.visionforge.html.*
import java.awt.Desktop
import java.net.URI
import kotlin.time.Duration.Companion.milliseconds
@ -57,210 +52,20 @@ public enum class DataServeMode {
UPDATE
}
/**
* A ktor plugin container with given [routing]
* @param serverUrl a server url including root route
*/
public class VisionServer internal constructor(
private val visionManager: VisionManager,
private val serverUrl: Url,
private val root: Route,
) : Configurable {
public class VisionRouteConfiguration(
public val visionManager: VisionManager,
override val meta: ObservableMutableMeta = MutableMeta(),
) : Configurable, ContextAware {
public val application: Application get() = root.application
override val meta: ObservableMutableMeta = MutableMeta()
override val context: Context get() = visionManager.context
/**
* 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)
/**
* 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)
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 const val DEFAULT_PORT: Int = 7777
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(
plugin: Plugin<P, B, F>,
configure: B.() -> Unit = {},
): 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

View File

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

View File

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