Refactor server API

This commit is contained in:
Alexander Nozik 2022-12-03 13:53:34 +03:00
parent 20b20a621f
commit c8141c6338
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
5 changed files with 240 additions and 191 deletions

View File

@ -2,6 +2,7 @@ package space.kscience.visionforge.examples
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.http.content.resources
import io.ktor.server.routing.routing
import kotlinx.html.*
import space.kscience.dataforge.context.Global
@ -10,6 +11,7 @@ import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.formFragment
import space.kscience.visionforge.onPropertyChange
import space.kscience.visionforge.server.EngineConnectorConfig
import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.visionPage
@ -17,10 +19,16 @@ import space.kscience.visionforge.server.visionPage
fun main() {
val visionManager = Global.fetch(VisionManager)
val server = embeddedServer(CIO, 7777, "localhost") {
val connector = EngineConnectorConfig("localhost", 7777)
val server = embeddedServer(CIO, connector.port, connector.host) {
routing {
visionPage(visionManager, VisionPage.scriptHeader("js/visionforge-playground.js")) {
resources()
}
visionPage(connector, visionManager, VisionPage.scriptHeader("js/visionforge-playground.js")) {
val form = formFragment("form") {
label {
htmlFor = "fname"
@ -58,7 +66,7 @@ fun main() {
println(this)
}
}
}
}.start(false)
server.openInBrowser()

View File

@ -15,6 +15,7 @@ import space.kscience.dataforge.meta.Null
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Colors
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.server.EngineConnectorConfig
import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.visionPage
@ -36,21 +37,26 @@ fun main() {
color.set(Colors.white)
}
}
val connector = EngineConnectorConfig("localhost", 7777)
val server = embeddedServer(CIO,7777, "localhost") {
val server = embeddedServer(CIO, connector.port, connector.host) {
routing {
static {
resources()
}
}
visionPage(solids.visionManager, VisionPage.threeJsHeader, VisionPage.styleSheetHeader("css/styles.css")) {
visionPage(
connector,
solids.visionManager, VisionPage.threeJsHeader,
VisionPage.styleSheetHeader("css/styles.css")
) {
div("flex-column") {
h1 { +"Satellite detector demo" }
vision { sat }
}
}
}
}.start(false)
server.openInBrowser()

View File

@ -4,9 +4,8 @@ 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.EngineConnectorConfig
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
@ -26,8 +25,8 @@ 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.VisionRouteConfiguration
import space.kscience.visionforge.server.require
import space.kscience.visionforge.server.EngineConnectorConfig
import space.kscience.visionforge.server.VisionRoute
import space.kscience.visionforge.server.serveVisionData
import space.kscience.visionforge.visionManager
import kotlin.coroutines.CoroutineContext
@ -48,8 +47,6 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
public val visionManager: VisionManager = context.visionManager
private val configuration = VisionRouteConfiguration(visionManager)
private var counter = 0
private var engine: ApplicationEngine? = null
@ -68,7 +65,7 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
public fun startServer(
host: String = context.properties["visionforge.host"].string ?: "localhost",
port: Int = context.properties["visionforge.port"].int ?: VisionRouteConfiguration.DEFAULT_PORT,
port: Int = context.properties["visionforge.port"].int ?: VisionRoute.DEFAULT_PORT,
): MimeTypedResult = html {
if (engine != null) {
p {
@ -77,6 +74,8 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
}
}
val connector: EngineConnectorConfig = EngineConnectorConfig(host, port)
engine?.stop(1000, 2000)
engine = context.embeddedServer(CIO, port, host) {
install(WebSockets)
@ -111,7 +110,7 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
val collector: VisionCollector = mutableMapOf()
val url = engine.environment.connectors.first().let {
url{
url {
protocol = URLProtocol.WS
host = it.host
port = it.port
@ -119,6 +118,8 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
}
}
engine.application.serveVisionData(VisionRoute(cellRoute, visionManager), collector)
visionFragment(
context,
embedData = true,
@ -126,13 +127,6 @@ public class VFForNotebook(override val context: Context) : ContextAware, Corout
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

@ -21,6 +21,7 @@ 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.*
@ -30,12 +31,16 @@ import space.kscience.visionforge.VisionChange
import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.flowChanges
import space.kscience.visionforge.html.*
import java.awt.Desktop
import java.net.URI
import kotlin.time.Duration.Companion.milliseconds
public enum class DataServeMode {
public class VisionRoute(
public val route: String,
public val visionManager: VisionManager,
override val meta: ObservableMutableMeta = MutableMeta(),
) : Configurable, ContextAware {
public enum class Mode {
/**
* Embed the initial state of the vision inside its html tag.
*/
@ -50,12 +55,7 @@ public enum class DataServeMode {
* Connect to server to get pushes. The address of the server is embedded in the tag.
*/
UPDATE
}
public class VisionRouteConfiguration(
public val visionManager: VisionManager,
override val meta: ObservableMutableMeta = MutableMeta(),
) : Configurable, ContextAware {
}
override val context: Context get() = visionManager.context
@ -64,7 +64,7 @@ public class VisionRouteConfiguration(
*/
public var updateInterval: Long by meta.long(300, key = UPDATE_INTERVAL_KEY)
public var dataMode: DataServeMode by meta.enum(DataServeMode.UPDATE)
public var dataMode: Mode by meta.enum(Mode.UPDATE)
public companion object {
public const val DEFAULT_PORT: Int = 7777
@ -78,11 +78,17 @@ public class VisionRouteConfiguration(
* Serve visions in a given [route] without providing a page template.
* [visions] could be changed during the service.
*/
public fun Route.serveVisionData(
configuration: VisionRouteConfiguration,
public fun Application.serveVisionData(
configuration: VisionRoute,
resolveVision: (Name) -> Vision?,
) {
application.log.info("Serving visions at ${this@serveVisionData}")
require(WebSockets)
routing {
route(configuration.route) {
install(CORS) {
anyHost()
}
application.log.info("Serving visions at ${configuration.route}")
//Update websocket
webSocket("ws") {
@ -131,12 +137,15 @@ public fun Route.serveVisionData(
)
}
}
}
}
}
public fun Route.serveVisionData(
configuration: VisionRouteConfiguration,
public fun Application.serveVisionData(
configuration: VisionRoute,
cache: VisionCollector,
): Unit = serveVisionData(configuration) { cache[it]?.second }
//
///**
// * Compile a fragment to string and serve visions from it
@ -161,24 +170,18 @@ public fun Route.serveVisionData(
/**
* Serve a page, potentially containing any number of visions at a given [route] with given [header].
*/
public fun Route.visionPage(
configuration: VisionRouteConfiguration,
public fun Application.visionPage(
route: String,
configuration: VisionRoute,
connector: EngineConnectorConfig,
headers: Collection<HtmlFragment>,
visionFragment: HtmlVisionFragment,
) {
application.require(WebSockets)
require(CORS) {
anyHost()
}
require(WebSockets)
val visionCache: VisionCollector = mutableMapOf()
serveVisionData(configuration, visionCache)
val collector: VisionCollector = mutableMapOf()
//filled pages
get {
//re-create html and vision list on each call
call.respondHtml {
val callbackUrl = call.url()
val html = createHTML().apply {
head {
meta {
charset = "utf-8"
@ -191,61 +194,61 @@ public fun Route.visionPage(
//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()
embedData = configuration.dataMode == VisionRoute.Mode.EMBED,
fetchDataUrl = if (configuration.dataMode != VisionRoute.Mode.EMBED) {
url {
host = connector.host
port = connector.port
path(route, "data")
}
} else null,
updatesUrl = if (configuration.dataMode == DataServeMode.UPDATE) {
URLBuilder(callbackUrl).apply {
updatesUrl = if (configuration.dataMode == VisionRoute.Mode.UPDATE) {
url {
protocol = URLProtocol.WS
pathSegments = pathSegments + "ws"
}.buildString()
host = connector.host
port = connector.port
path(route, "ws")
}
} else null,
visionCache = visionCache,
visionCache = collector,
fragment = visionFragment
)
}
}
}.finalize()
//serve data
serveVisionData(configuration, collector)
//filled pages
routing {
get(route) {
call.respondText(html, ContentType.Text.Html)
}
}
}
public fun Route.visionPage(
public fun Application.visionPage(
connector: EngineConnectorConfig,
visionManager: VisionManager,
vararg headers: HtmlFragment,
configurationBuilder: VisionRouteConfiguration.() -> Unit = {},
route: String = "/",
configurationBuilder: VisionRoute.() -> Unit = {},
visionFragment: HtmlVisionFragment,
) {
val configuration = VisionRouteConfiguration(visionManager).apply(configurationBuilder)
visionPage(configuration, listOf(*headers), visionFragment)
val configuration = VisionRoute(route, visionManager).apply(configurationBuilder)
visionPage(route, configuration, connector, 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 Application.visionPage(
connector: EngineConnectorConfig,
page: VisionPage,
route: String = "/",
configurationBuilder: VisionRoute.() -> Unit = {},
) {
val configuration = VisionRoute(route, page.visionManager).apply(configurationBuilder)
visionPage(route, configuration, connector, 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)
/**
* Connect to a given Ktor server using browser
*/
public fun ApplicationEngine.openInBrowser() {
val connector = environment.connectors.first()
val uri = URI("http", null, connector.host, connector.port, null, null, null)
Desktop.getDesktop().browse(uri)
}
/**
* Stop the server with default timeouts
*/
public fun ApplicationEngine.close(): Unit = stop(1000, 5000)

View File

@ -0,0 +1,38 @@
package space.kscience.visionforge.server
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.Plugin
import io.ktor.server.application.install
import io.ktor.server.application.pluginOrNull
import io.ktor.server.engine.ApplicationEngine
import io.ktor.server.engine.EngineConnectorBuilder
import io.ktor.server.engine.EngineConnectorConfig
import io.ktor.util.pipeline.Pipeline
import java.awt.Desktop
import java.net.URI
public fun <P : Pipeline<*, ApplicationCall>, B : Any, F : Any> P.require(
plugin: Plugin<P, B, F>,
): F = pluginOrNull(plugin) ?: install(plugin)
/**
* Connect to a given Ktor server using browser
*/
public fun ApplicationEngine.openInBrowser() {
val connector = environment.connectors.first()
val uri = URI("http", null, connector.host, connector.port, null, null, null)
Desktop.getDesktop().browse(uri)
}
/**
* Stop the server with default timeouts
*/
public fun ApplicationEngine.close(): Unit = stop(1000, 5000)
public fun EngineConnectorConfig(host: String, port: Int): EngineConnectorConfig = EngineConnectorBuilder().apply {
this.host = host
this.port = port
}