Move to stand-alone sse
This commit is contained in:
parent
599d08b62a
commit
8c8d53b187
@ -18,10 +18,5 @@ kotlin {
|
|||||||
api("hep.dataforge:dataforge-io:$dataforgeVersion")
|
api("hep.dataforge:dataforge-io:$dataforgeVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvmTest{
|
|
||||||
dependencies{
|
|
||||||
api("io.ktor:ktor-network:$ktorVersion")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,26 +0,0 @@
|
|||||||
package hep.dataforge.control.server
|
|
||||||
|
|
||||||
import hep.dataforge.control.sse.SseEvent
|
|
||||||
import hep.dataforge.control.sse.writeSseFlow
|
|
||||||
import io.ktor.application.ApplicationCall
|
|
||||||
import io.ktor.http.CacheControl
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.response.cacheControl
|
|
||||||
import io.ktor.response.respondBytesWriter
|
|
||||||
import io.ktor.util.KtorExperimentalAPI
|
|
||||||
import kotlinx.coroutines.channels.ReceiveChannel
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method that responds an [ApplicationCall] by reading all the [SseEvent]s from the specified [events] [ReceiveChannel]
|
|
||||||
* and serializing them in a way that is compatible with the Server-Sent Events specification.
|
|
||||||
*
|
|
||||||
* You can read more about it here: https://www.html5rocks.com/en/tutorials/eventsource/basics/
|
|
||||||
*/
|
|
||||||
@OptIn(KtorExperimentalAPI::class)
|
|
||||||
public suspend fun ApplicationCall.respondSse(events: Flow<SseEvent>) {
|
|
||||||
response.cacheControl(CacheControl.NoCache(null))
|
|
||||||
respondBytesWriter(contentType = ContentType.Text.EventStream) {
|
|
||||||
writeSseFlow(events)
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,12 +17,6 @@ kotlin {
|
|||||||
api("io.ktor:ktor-network:$ktorVersion")
|
api("io.ktor:ktor-network:$ktorVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvmTest{
|
|
||||||
dependencies{
|
|
||||||
implementation("io.ktor:ktor-server-cio:$ktorVersion")
|
|
||||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
|
||||||
implementation("ch.qos.logback:logback-classic:1.2.3")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,60 +0,0 @@
|
|||||||
package hep.dataforge.control.sse
|
|
||||||
|
|
||||||
import io.ktor.utils.io.*
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The data class representing a SSE Event that will be sent to the client.
|
|
||||||
*/
|
|
||||||
public data class SseEvent(val data: String, val event: String? = null, val id: String? = null)
|
|
||||||
|
|
||||||
public suspend fun ByteWriteChannel.writeSseFlow(events: Flow<SseEvent>): Unit = events.collect { event ->
|
|
||||||
if (event.id != null) {
|
|
||||||
writeStringUtf8("id: ${event.id}\n")
|
|
||||||
}
|
|
||||||
if (event.event != null) {
|
|
||||||
writeStringUtf8("event: ${event.event}\n")
|
|
||||||
}
|
|
||||||
for (dataLine in event.data.lines()) {
|
|
||||||
writeStringUtf8("data: $dataLine\n")
|
|
||||||
}
|
|
||||||
writeStringUtf8("\n")
|
|
||||||
flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
public suspend fun ByteReadChannel.readSseFlow(): Flow<SseEvent> = channelFlow {
|
|
||||||
while (isActive) {
|
|
||||||
//val lines = ArrayList<String>()
|
|
||||||
val builder = StringBuilder()
|
|
||||||
var id: String? = null
|
|
||||||
var event: String? = null
|
|
||||||
//read lines until blank line or the end of stream
|
|
||||||
|
|
||||||
do{
|
|
||||||
val line = readUTF8Line()
|
|
||||||
if (line != null && line.isNotBlank()) {
|
|
||||||
val key = line.substringBefore(":")
|
|
||||||
val value = line.substringAfter(": ")
|
|
||||||
when (key) {
|
|
||||||
"id" -> id = value
|
|
||||||
"event" -> event = value
|
|
||||||
"data" -> builder.append(value)
|
|
||||||
else -> error("Unrecognized event-stream key $key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (line?.isBlank() != true)
|
|
||||||
if(builder.isNotBlank()) {
|
|
||||||
send(SseEvent(builder.toString(), event, id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
awaitClose {
|
|
||||||
this@readSseFlow.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,102 +0,0 @@
|
|||||||
package hep.dataforge.control.ports
|
|
||||||
|
|
||||||
import hep.dataforge.context.Global
|
|
||||||
import io.ktor.network.selector.ActorSelectorManager
|
|
||||||
import io.ktor.network.sockets.aSocket
|
|
||||||
import io.ktor.network.sockets.openReadChannel
|
|
||||||
import io.ktor.network.sockets.openWriteChannel
|
|
||||||
import io.ktor.util.KtorExperimentalAPI
|
|
||||||
import io.ktor.util.cio.write
|
|
||||||
import io.ktor.utils.io.readUTF8Line
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
|
|
||||||
@OptIn(KtorExperimentalAPI::class)
|
|
||||||
fun CoroutineScope.launchEchoServer(port: Int): Job = launch {
|
|
||||||
val server = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind(InetSocketAddress("localhost", port))
|
|
||||||
println("Started echo telnet server at ${server.localAddress}")
|
|
||||||
|
|
||||||
while (isActive) {
|
|
||||||
val socket = try {
|
|
||||||
server.accept()
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
server.close()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
launch {
|
|
||||||
println("Socket accepted: ${socket.remoteAddress}")
|
|
||||||
|
|
||||||
try {
|
|
||||||
val input = socket.openReadChannel()
|
|
||||||
val output = socket.openWriteChannel(autoFlush = true)
|
|
||||||
|
|
||||||
|
|
||||||
while (isActive) {
|
|
||||||
val line = input.readUTF8Line()
|
|
||||||
|
|
||||||
//println("${socket.remoteAddress}: $line")
|
|
||||||
output.write("[response] $line")
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
cancel()
|
|
||||||
} finally {
|
|
||||||
socket.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class TcpPortTest {
|
|
||||||
@Test
|
|
||||||
fun testWithEchoServer() {
|
|
||||||
try {
|
|
||||||
runBlocking {
|
|
||||||
val server = launchEchoServer(22188)
|
|
||||||
val port = TcpPort.open(Global, "localhost", 22188)
|
|
||||||
|
|
||||||
val logJob = launch {
|
|
||||||
port.receiving().collect {
|
|
||||||
println("Flow: ${it.decodeToString()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
port.startJob.join()
|
|
||||||
port.send("aaa\n")
|
|
||||||
port.send("ddd\n")
|
|
||||||
|
|
||||||
delay(200)
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (ex !is CancellationException) throw ex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testKtorWithEchoServer() {
|
|
||||||
try {
|
|
||||||
runBlocking {
|
|
||||||
val server = launchEchoServer(22188)
|
|
||||||
val port = KtorTcpPort.open(Global,"localhost", 22188)
|
|
||||||
|
|
||||||
val logJob = launch {
|
|
||||||
port.receiving().collect {
|
|
||||||
println("Flow: ${it.decodeToString()}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
port.send("aaa\n")
|
|
||||||
port.send("ddd\n")
|
|
||||||
|
|
||||||
delay(200)
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (ex !is CancellationException) throw ex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
package hep.dataforge.control.sse
|
|
||||||
|
|
||||||
import io.ktor.application.ApplicationCall
|
|
||||||
import io.ktor.application.call
|
|
||||||
import io.ktor.client.HttpClient
|
|
||||||
import io.ktor.client.call.receive
|
|
||||||
import io.ktor.client.request.get
|
|
||||||
import io.ktor.client.statement.HttpResponse
|
|
||||||
import io.ktor.client.statement.HttpStatement
|
|
||||||
import io.ktor.http.CacheControl
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.response.cacheControl
|
|
||||||
import io.ktor.response.respondBytesWriter
|
|
||||||
import io.ktor.routing.get
|
|
||||||
import io.ktor.routing.routing
|
|
||||||
import io.ktor.server.cio.CIO
|
|
||||||
import io.ktor.server.engine.embeddedServer
|
|
||||||
import io.ktor.util.KtorExperimentalAPI
|
|
||||||
import io.ktor.utils.io.ByteReadChannel
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
|
|
||||||
@OptIn(KtorExperimentalAPI::class)
|
|
||||||
suspend fun ApplicationCall.respondSse(events: Flow<SseEvent>) {
|
|
||||||
response.cacheControl(CacheControl.NoCache(null))
|
|
||||||
respondBytesWriter(contentType = ContentType.Text.EventStream) {
|
|
||||||
writeSseFlow(events)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun HttpClient.readSse(address: String, block: suspend (SseEvent) -> Unit): Job = launch {
|
|
||||||
get<HttpStatement>(address).execute { response: HttpResponse ->
|
|
||||||
// Response is not downloaded here.
|
|
||||||
val channel = response.receive<ByteReadChannel>()
|
|
||||||
val flow = channel.readSseFlow()
|
|
||||||
flow.collect(block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SseTest {
|
|
||||||
@OptIn(KtorExperimentalAPI::class)
|
|
||||||
@Test
|
|
||||||
fun testSseIntegration() {
|
|
||||||
runBlocking(Dispatchers.Default) {
|
|
||||||
val server = embeddedServer(CIO, 12080) {
|
|
||||||
routing {
|
|
||||||
get("/") {
|
|
||||||
val flow = flow {
|
|
||||||
repeat(5) {
|
|
||||||
delay(300)
|
|
||||||
emit(it)
|
|
||||||
}
|
|
||||||
}.map {
|
|
||||||
SseEvent(data = it.toString(), id = it.toString())
|
|
||||||
}
|
|
||||||
call.respondSse(flow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
server.start(wait = false)
|
|
||||||
delay(1000)
|
|
||||||
val client = HttpClient(io.ktor.client.engine.cio.CIO)
|
|
||||||
client.readSse("http://localhost:12080") {
|
|
||||||
println(it)
|
|
||||||
}
|
|
||||||
delay(2000)
|
|
||||||
println("Closing the client after waiting")
|
|
||||||
client.close()
|
|
||||||
server.stop(1000, 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,6 +5,10 @@ plugins {
|
|||||||
|
|
||||||
val ktorVersion: String by rootProject.extra
|
val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
|
repositories{
|
||||||
|
maven("https://maven.pkg.github.com/altavir/ktor-client-sse")
|
||||||
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
@ -12,11 +16,12 @@ kotlin {
|
|||||||
implementation(project(":dataforge-device-core"))
|
implementation(project(":dataforge-device-core"))
|
||||||
implementation(project(":dataforge-device-tcp"))
|
implementation(project(":dataforge-device-tcp"))
|
||||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||||
|
implementation("ru.mipt.npm:ktor-client-sse:0.1.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jvmMain {
|
jvmMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsMain {
|
jsMain {
|
||||||
|
@ -3,38 +3,23 @@ package hep.dataforge.control.client
|
|||||||
import hep.dataforge.control.controllers.DeviceManager
|
import hep.dataforge.control.controllers.DeviceManager
|
||||||
import hep.dataforge.control.controllers.DeviceMessage
|
import hep.dataforge.control.controllers.DeviceMessage
|
||||||
import hep.dataforge.control.controllers.respondMessage
|
import hep.dataforge.control.controllers.respondMessage
|
||||||
import hep.dataforge.control.sse.SseEvent
|
|
||||||
import hep.dataforge.control.sse.readSseFlow
|
|
||||||
import hep.dataforge.meta.toJson
|
import hep.dataforge.meta.toJson
|
||||||
import hep.dataforge.meta.toMeta
|
import hep.dataforge.meta.toMeta
|
||||||
import hep.dataforge.meta.wrap
|
import hep.dataforge.meta.wrap
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.receive
|
|
||||||
import io.ktor.client.request.get
|
|
||||||
import io.ktor.client.request.post
|
import io.ktor.client.request.post
|
||||||
import io.ktor.client.statement.HttpResponse
|
|
||||||
import io.ktor.client.statement.HttpStatement
|
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.Url
|
import io.ktor.http.Url
|
||||||
import io.ktor.http.contentType
|
import io.ktor.http.contentType
|
||||||
import io.ktor.utils.io.ByteReadChannel
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.serialization.json.*
|
import kotlinx.serialization.json.*
|
||||||
|
import ru.mipt.npm.ktor.sse.readSse
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
|
||||||
private suspend fun HttpClient.readSse(address: String, block: suspend (SseEvent) -> Unit): Job = launch {
|
|
||||||
get<HttpStatement>(address).execute { response: HttpResponse ->
|
|
||||||
// Response is not downloaded here.
|
|
||||||
val channel = response.receive<ByteReadChannel>()
|
|
||||||
val flow = channel.readSseFlow()
|
|
||||||
flow.collect(block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
"id":"string|number[optional, but desired]",
|
"id":"string|number[optional, but desired]",
|
||||||
@ -53,11 +38,12 @@ private suspend fun HttpClient.readSse(address: String, block: suspend (SseEvent
|
|||||||
public class MagixClient(
|
public class MagixClient(
|
||||||
private val manager: DeviceManager,
|
private val manager: DeviceManager,
|
||||||
private val postUrl: Url,
|
private val postUrl: Url,
|
||||||
private val sseUrl: Url
|
private val sseUrl: Url,
|
||||||
//private val inbox: Flow<JsonObject>
|
//private val inbox: Flow<JsonObject>
|
||||||
) : CoroutineScope {
|
) : CoroutineScope {
|
||||||
|
|
||||||
override val coroutineContext: CoroutineContext = manager.context.coroutineContext + Job(manager.context.coroutineContext[Job])
|
override val coroutineContext: CoroutineContext =
|
||||||
|
manager.context.coroutineContext + Job(manager.context.coroutineContext[Job])
|
||||||
|
|
||||||
private val client = HttpClient()
|
private val client = HttpClient()
|
||||||
|
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -3,10 +3,15 @@ import ru.mipt.npm.gradle.useFx
|
|||||||
plugins {
|
plugins {
|
||||||
id("ru.mipt.npm.jvm")
|
id("ru.mipt.npm.jvm")
|
||||||
id("ru.mipt.npm.publish")
|
id("ru.mipt.npm.publish")
|
||||||
|
application
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO to be moved to a separate project
|
//TODO to be moved to a separate project
|
||||||
|
|
||||||
|
application{
|
||||||
|
mainClassName = "ru.mipt.npm.devices.pimotionmaster.PiMotionMasterAppKt"
|
||||||
|
}
|
||||||
|
|
||||||
kotlin{
|
kotlin{
|
||||||
explicitApi = null
|
explicitApi = null
|
||||||
useFx(ru.mipt.npm.gradle.FXModule.CONTROLS, configuration = ru.mipt.npm.gradle.DependencyConfiguration.IMPLEMENTATION)
|
useFx(ru.mipt.npm.gradle.FXModule.CONTROLS, configuration = ru.mipt.npm.gradle.DependencyConfiguration.IMPLEMENTATION)
|
||||||
|
@ -111,7 +111,7 @@ class PiMotionMasterView : View() {
|
|||||||
action {
|
action {
|
||||||
if (!debugServerStarted.get()) {
|
if (!debugServerStarted.get()) {
|
||||||
debugServerJobProperty.value =
|
debugServerJobProperty.value =
|
||||||
controller.context.launchPiDebugServer(port.get(), listOf("1", "2"))
|
controller.context.launchPiDebugServer(port.get(), listOf("1", "2", "3", "4"))
|
||||||
} else {
|
} else {
|
||||||
debugServerJobProperty.get().cancel()
|
debugServerJobProperty.get().cancel()
|
||||||
debugServerJobProperty.value = null
|
debugServerJobProperty.value = null
|
||||||
|
@ -3,10 +3,10 @@ pluginManagement {
|
|||||||
val toolsVersion = "0.6.3-dev-1.4.20-M1"
|
val toolsVersion = "0.6.3-dev-1.4.20-M1"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
//mavenLocal()
|
mavenLocal()
|
||||||
jcenter()
|
jcenter()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
maven("https://kotlin.bintray.com/kotlinx")
|
//maven("https://kotlin.bintray.com/kotlinx")
|
||||||
maven("https://dl.bintray.com/kotlin/kotlin-eap")
|
maven("https://dl.bintray.com/kotlin/kotlin-eap")
|
||||||
maven("https://dl.bintray.com/mipt-npm/dataforge")
|
maven("https://dl.bintray.com/mipt-npm/dataforge")
|
||||||
maven("https://dl.bintray.com/mipt-npm/kscience")
|
maven("https://dl.bintray.com/mipt-npm/kscience")
|
||||||
@ -32,7 +32,7 @@ include(
|
|||||||
":dataforge-device-serial",
|
":dataforge-device-serial",
|
||||||
":dataforge-device-server",
|
":dataforge-device-server",
|
||||||
":dataforge-magix-client",
|
":dataforge-magix-client",
|
||||||
":demo",
|
// ":demo",
|
||||||
":motors"
|
":motors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user