refactor websocket/tile server
This commit is contained in:
parent
8f24bfb797
commit
0c6f2a4bfe
15 changed files with 556 additions and 414 deletions
|
|
@ -1,23 +1,26 @@
|
|||
package dev.adamko.kafkatorio.processor
|
||||
|
||||
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
|
||||
import dev.adamko.kafkatorio.processor.tileserver.WebsocketServer
|
||||
import dev.adamko.kafkatorio.processor.topology.broadcastToWebsocket
|
||||
import dev.adamko.kafkatorio.processor.topology.colourMapChunks
|
||||
import dev.adamko.kafkatorio.processor.topology.factorioServerPacketStream
|
||||
import dev.adamko.kafkatorio.processor.topology.broadcastToWebsocket
|
||||
import dev.adamko.kafkatorio.processor.topology.saveMapTiles
|
||||
import dev.adamko.kafkatorio.processor.topology.splitFactorioServerPacketStream
|
||||
import java.time.Duration
|
||||
import java.util.Properties
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.apache.kafka.clients.producer.ProducerConfig
|
||||
import org.apache.kafka.streams.KafkaStreams
|
||||
import org.apache.kafka.streams.StreamsBuilder
|
||||
import org.apache.kafka.streams.StreamsConfig
|
||||
|
|
@ -27,13 +30,12 @@ import org.apache.kafka.streams.TopologyDescription
|
|||
|
||||
internal class KafkatorioTopology(
|
||||
private val websocketServer: WebsocketServer,
|
||||
private val appProps: ApplicationProperties = ApplicationProperties(),
|
||||
) : CoroutineScope {
|
||||
private val appProps: ApplicationProperties,
|
||||
) {
|
||||
|
||||
override val coroutineContext: CoroutineContext =
|
||||
Dispatchers.Default + rootJob + CoroutineName("KafkatorioTopology")
|
||||
private val builderScope = CoroutineScope(CoroutineName("KafkaTopology"))
|
||||
|
||||
fun start() {
|
||||
suspend fun start() = builderScope.launch {
|
||||
splitPackets()
|
||||
|
||||
groupTilesMapChunks()
|
||||
|
|
@ -42,7 +44,7 @@ internal class KafkatorioTopology(
|
|||
websocketBroadcaster()
|
||||
}
|
||||
|
||||
private fun splitPackets() {
|
||||
private suspend fun splitPackets() {
|
||||
val builder = StreamsBuilder()
|
||||
val packets = factorioServerPacketStream(builder)
|
||||
splitFactorioServerPacketStream(packets)
|
||||
|
|
@ -52,45 +54,77 @@ internal class KafkatorioTopology(
|
|||
launchTopology("splitPackets", topology)
|
||||
}
|
||||
|
||||
private fun groupTilesMapChunks() {
|
||||
private suspend fun groupTilesMapChunks() {
|
||||
val builder = StreamsBuilder()
|
||||
val topology = colourMapChunks(builder)
|
||||
launchTopology("groupTilesMapChunks", topology)
|
||||
}
|
||||
|
||||
private fun saveTiles() {
|
||||
private suspend fun saveTiles() {
|
||||
val builder = StreamsBuilder()
|
||||
val topology = saveMapTiles(builder, appProps.webmapTileDir)
|
||||
val topology = saveMapTiles(builder, appProps.tileDir)
|
||||
launchTopology("saveTiles", topology)
|
||||
}
|
||||
|
||||
private fun websocketBroadcaster() {
|
||||
private suspend fun websocketBroadcaster() {
|
||||
val builder = StreamsBuilder()
|
||||
broadcastToWebsocket(websocketServer, builder)
|
||||
|
||||
val topology = builder.build()
|
||||
|
||||
launchTopology("websocketBroadcaster", topology)
|
||||
launchTopology(
|
||||
"websocketBroadcaster", topology, mapOf(
|
||||
StreamsConfig.COMMIT_INTERVAL_MS_CONFIG to "${1.seconds.inWholeMilliseconds}",
|
||||
StreamsConfig.producerPrefix(ProducerConfig.LINGER_MS_CONFIG) to "${10.milliseconds.inWholeMilliseconds}"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun launchTopology(
|
||||
private suspend fun launchTopology(
|
||||
id: String,
|
||||
topology: Topology,
|
||||
config: Map<String, String> = mapOf(),
|
||||
additionalProperties: Map<String, String> = mapOf(),
|
||||
) {
|
||||
|
||||
val props: Properties = appProps.kafkaConfig
|
||||
val props: Properties = appProps.kafkaStreamsConfig.toProperties()
|
||||
|
||||
config.forEach { (k, v) -> props.setProperty(k, v) }
|
||||
additionalProperties.forEach { (k, v) -> props.setProperty(k, v) }
|
||||
|
||||
val appId = props.compute(StreamsConfig.APPLICATION_ID_CONFIG) { _, v -> "$v.$id" } as? String
|
||||
val appId = props.compute(StreamsConfig.APPLICATION_ID_CONFIG) { _, v -> "$v.$id" } as String
|
||||
props.setProperty(StreamsConfig.APPLICATION_ID_CONFIG, appId)
|
||||
|
||||
val streams = KafkaStreams(topology, props)
|
||||
streams.setUncaughtExceptionHandler(StreamsExceptionHandler {
|
||||
coroutineContext.job.cancel(CancellationException(cause = it))
|
||||
})
|
||||
builderScope.launch(CoroutineName(appId)) {
|
||||
|
||||
val streamsState: KafkaStreams.State = suspendCancellableCoroutine { cont ->
|
||||
|
||||
val streams = KafkaStreams(topology, props)
|
||||
streams.setUncaughtExceptionHandler(StreamsExceptionHandler { e ->
|
||||
cont.resumeWithException(e)
|
||||
})
|
||||
|
||||
streams.setStateListener { newState: KafkaStreams.State, _ ->
|
||||
when (newState) {
|
||||
KafkaStreams.State.NOT_RUNNING,
|
||||
KafkaStreams.State.ERROR -> cont.resume(newState)
|
||||
else -> Unit // do nothing
|
||||
}
|
||||
}
|
||||
|
||||
cont.invokeOnCancellation {
|
||||
println("[$appId] coroutine cancelled -> closing. cause:$it")
|
||||
streams.close()
|
||||
}
|
||||
|
||||
printTopologyDescription(appId, topology)
|
||||
streams.start()
|
||||
println("launched Topology $appId")
|
||||
}
|
||||
println("exiting Topology $appId: $streamsState")
|
||||
currentCoroutineContext().job.cancel("$appId: $streamsState")
|
||||
}
|
||||
}
|
||||
|
||||
private fun printTopologyDescription(appId: String, topology: Topology) {
|
||||
val description: TopologyDescription = topology.describe()
|
||||
|
||||
val descFile = Path("./build/$appId.txt").toFile().apply {
|
||||
|
|
@ -107,19 +141,12 @@ internal class KafkatorioTopology(
|
|||
|----------------
|
||||
""".trimMargin()
|
||||
)
|
||||
|
||||
coroutineContext.job.invokeOnCompletion {
|
||||
println("closing $id KafkaStreams")
|
||||
rootJob.cancelChildren()
|
||||
rootJob.cancel()
|
||||
streams.close(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
launch { streams.start() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
val rootJob = SupervisorJob()
|
||||
private fun Map<String, String>.toProperties(): Properties =
|
||||
entries.fold(Properties()) { props, (k, v) ->
|
||||
props.apply { setProperty(k, v) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
package dev.adamko.kafkatorio.processor
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.withIndex
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import org.http4k.routing.bind
|
||||
import org.http4k.routing.websockets
|
||||
import org.http4k.websocket.Websocket
|
||||
import org.http4k.websocket.WsHandler
|
||||
import org.http4k.websocket.WsMessage
|
||||
import org.http4k.websocket.WsStatus
|
||||
|
||||
|
||||
class WebsocketServer {
|
||||
|
||||
// create a scope+job for the server
|
||||
private val serverJob = KafkatorioTopology.rootJob
|
||||
private val serverScope = CoroutineScope(serverJob)
|
||||
|
||||
private val clients: MutableSharedFlow<Websocket> = MutableSharedFlow(
|
||||
replay = 100,
|
||||
extraBufferCapacity = 100,
|
||||
BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
private val messages: MutableSharedFlow<String> = MutableSharedFlow(
|
||||
replay = 100,
|
||||
extraBufferCapacity = 100,
|
||||
BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
private fun bindClient(ws: Websocket) = runBlocking(serverScope.coroutineContext) {
|
||||
clients.emit(ws)
|
||||
}
|
||||
|
||||
fun sendMessage(msg: String) = runBlocking(serverScope.coroutineContext) {
|
||||
messages.emit(msg)
|
||||
}
|
||||
|
||||
suspend fun build(): WsHandler = supervisorScope builder@{
|
||||
|
||||
coroutineContext.job.invokeOnCompletion { e ->
|
||||
e?.let { serverJob.completeExceptionally(it) } ?: serverJob.complete()
|
||||
}
|
||||
|
||||
clients
|
||||
.withIndex()
|
||||
.onEach { (id, ws) ->
|
||||
// create a scope+job for each client
|
||||
val clientJob = SupervisorJob(serverJob)
|
||||
val clientScope = CoroutineScope(clientJob)
|
||||
|
||||
clientJob.invokeOnCompletion { e ->
|
||||
println("[clientJob.invokeOnCompletion] closing client ws $e")
|
||||
when (e) {
|
||||
null -> ws.close(WsStatus.NORMAL)
|
||||
else -> ws.close(WsStatus.ABNORMAL_CLOSE)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onError { e ->
|
||||
sendMessage("[ws.onError] client $id error [${e}]")
|
||||
clientJob.completeExceptionally(e)
|
||||
}
|
||||
ws.onClose { status ->
|
||||
sendMessage("[ws.onClose] client $id disconnected [${status}]")
|
||||
clientJob.complete()
|
||||
}
|
||||
ws.onMessage { wsMsg ->
|
||||
sendMessage("$id says: ${wsMsg.bodyString()}")
|
||||
}
|
||||
|
||||
sendMessage("new client $id!")
|
||||
|
||||
messages
|
||||
.onEach { msg -> ws.send(WsMessage(msg)) }
|
||||
.launchIn(clientScope)
|
||||
|
||||
}
|
||||
.launchIn(serverScope)
|
||||
|
||||
|
||||
return@builder websockets("/ws" bind ::bindClient)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package dev.adamko.kafkatorio.processor.admin
|
||||
|
||||
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
|
||||
import java.util.concurrent.TimeUnit
|
||||
import mu.KotlinLogging
|
||||
import org.apache.kafka.clients.admin.Admin
|
||||
|
|
@ -8,17 +9,17 @@ import org.apache.kafka.clients.admin.NewTopic
|
|||
import org.apache.kafka.clients.admin.TopicListing
|
||||
|
||||
|
||||
object KafkatorioKafkaAdmin {
|
||||
class KafkatorioKafkaAdmin(
|
||||
appProps: ApplicationProperties
|
||||
) {
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private val kafkaAdmin: Admin =
|
||||
Admin.create(
|
||||
mapOf(
|
||||
"bootstrap.servers" to "http://localhost:9092",
|
||||
"client.id" to "kafkatorio.setup-admin",
|
||||
)
|
||||
)
|
||||
init {
|
||||
println("Creating KafkatorioKafkaAdmin with config: ${appProps.kafkaAdminConfig}")
|
||||
}
|
||||
|
||||
private val kafkaAdmin: Admin = Admin.create(appProps.kafkaAdminConfig)
|
||||
|
||||
fun createKafkatorioTopics() {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,64 +1,94 @@
|
|||
package dev.adamko.kafkatorio.processor.config
|
||||
|
||||
import dev.adamko.kafkatorio.processor.config.PortVal.Companion.portVal
|
||||
import com.sksamuel.hoplite.ConfigLoaderBuilder
|
||||
import com.sksamuel.hoplite.addEnvironmentSource
|
||||
import com.sksamuel.hoplite.addResourceOrFileSource
|
||||
import java.nio.file.Path
|
||||
import java.time.Clock
|
||||
import java.util.Properties
|
||||
import kotlin.io.path.Path
|
||||
import org.http4k.cloudnative.env.Environment
|
||||
import org.http4k.cloudnative.env.EnvironmentKey
|
||||
import org.http4k.lens.nonEmptyString
|
||||
|
||||
/**
|
||||
* Load properties, in descending priority order, from
|
||||
*
|
||||
* 1. Environment variables
|
||||
* 2. This app's env file (as defined by the environment variable [Lenses.APPLICATION_PROPERTIES_FILE_PATH])
|
||||
* # 3. A resources-dir yaml file
|
||||
*
|
||||
*/
|
||||
internal class ApplicationProperties(
|
||||
vararg environments: Environment = arrayOf(
|
||||
Environment.env(),
|
||||
Environment.fromEnvVarPath(),
|
||||
Environment.fromResourcesFile(".secret.config.yml")
|
||||
)
|
||||
|
||||
// loaded by hoplite
|
||||
data class ApplicationProperties(
|
||||
val webPort: PortVal,
|
||||
val tileDir: Path,
|
||||
val kafkaAdminConfig: Map<String, String>,
|
||||
val kafkaStreamsConfig: Map<String, String>,
|
||||
) {
|
||||
|
||||
|
||||
private val appProps: Environment =
|
||||
environments.reduce { acc, environment -> acc.overrides(environment) }
|
||||
|
||||
|
||||
fun clock(): Clock = Clock.systemUTC()
|
||||
|
||||
|
||||
/**
|
||||
* The port of the server
|
||||
*
|
||||
* Set by GCP ([docs](https://cloud.google.com/run/docs/reference/container-contract#env-vars)).
|
||||
*/
|
||||
val serverPort: PortVal
|
||||
get() = EnvironmentKey.portVal().defaulted("PORT", PortVal.DEFAULT)[appProps]
|
||||
|
||||
|
||||
val kafkaConfig: Properties
|
||||
get() = EnvironmentKey
|
||||
.nonEmptyString()
|
||||
.map { srcString ->
|
||||
srcString.reader().use { stringReader ->
|
||||
Properties().also { properties ->
|
||||
properties.load(stringReader)
|
||||
}
|
||||
}
|
||||
}
|
||||
.required("kafka.config")[appProps]
|
||||
|
||||
|
||||
val webmapTileDir: Path
|
||||
get() = EnvironmentKey
|
||||
.nonEmptyString()
|
||||
.map(::Path)
|
||||
.required("tileServer.webmapTileDir")[appProps]
|
||||
|
||||
companion object {
|
||||
/** use Hoplite to load config from environment variables or properties files */
|
||||
fun load(): ApplicationProperties =
|
||||
ConfigLoaderBuilder.default()
|
||||
.addDefaults()
|
||||
.allowUnresolvedSubstitutions()
|
||||
.allowEmptyTree()
|
||||
.addEnvironmentSource()
|
||||
.addResourceOrFileSource("/.secret.config.yml", optional = true)
|
||||
.addResourceOrFileSource("/config.yml", optional = false)
|
||||
.build()
|
||||
.loadConfigOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//import dev.adamko.kafkatorio.processor.config.PortVal.Companion.portVal
|
||||
//import java.nio.file.Path
|
||||
//import java.time.Clock
|
||||
//import java.util.Properties
|
||||
//import kotlin.io.path.Path
|
||||
//import org.http4k.cloudnative.env.Environment
|
||||
//import org.http4k.cloudnative.env.EnvironmentKey
|
||||
//import org.http4k.lens.nonEmptyString
|
||||
//
|
||||
///**
|
||||
// * Load properties, in descending priority order, from
|
||||
// *
|
||||
// * 1. Environment variables
|
||||
// * 2. This app's env file (as defined by the environment variable [Lenses.APPLICATION_PROPERTIES_FILE_PATH])
|
||||
// * # 3. A resources-dir yaml file
|
||||
// *
|
||||
// */
|
||||
//internal class ApplicationProperties(
|
||||
// vararg environments: Environment = arrayOf(
|
||||
// Environment.env(),
|
||||
// Environment.fromEnvVarPath(),
|
||||
// Environment.fromResourcesFile(".secret.config.yml")
|
||||
// )
|
||||
//) {
|
||||
//
|
||||
//
|
||||
// private val appProps: Environment =
|
||||
// environments.reduce { acc, environment -> acc.overrides(environment) }
|
||||
//
|
||||
//
|
||||
// fun clock(): Clock = Clock.systemUTC()
|
||||
//
|
||||
//
|
||||
// /**
|
||||
// * The port of the server
|
||||
// *
|
||||
// * Set by GCP ([docs](https://cloud.google.com/run/docs/reference/container-contract#env-vars)).
|
||||
// */
|
||||
// val serverPort: PortVal
|
||||
// get() = EnvironmentKey.portVal().defaulted("PORT", PortVal.DEFAULT)[appProps]
|
||||
//
|
||||
//
|
||||
// val kafkaConfig: Properties
|
||||
// get() = EnvironmentKey
|
||||
// .nonEmptyString()
|
||||
// .map { srcString ->
|
||||
// srcString.reader().use { stringReader ->
|
||||
// Properties().also { properties ->
|
||||
// properties.load(stringReader)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .required("kafka.config")[appProps]
|
||||
//
|
||||
//
|
||||
// val webmapTileDir: Path
|
||||
// get() = EnvironmentKey
|
||||
// .nonEmptyString()
|
||||
// .map(::Path)
|
||||
// .required("tileServer.webmapTileDir")[appProps]
|
||||
//
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,39 @@
|
|||
package dev.adamko.kafkatorio.processor.config
|
||||
|
||||
import java.io.File
|
||||
import org.http4k.cloudnative.env.Environment
|
||||
import org.http4k.cloudnative.env.EnvironmentKey
|
||||
import org.http4k.core.Credentials
|
||||
import org.http4k.lens.BiDiLens
|
||||
import org.http4k.lens.BiDiMapping
|
||||
|
||||
|
||||
internal object Lenses {
|
||||
|
||||
/** The location of this app's env file. */
|
||||
val APPLICATION_PROPERTIES_FILE_PATH: BiDiLens<Environment, File?> =
|
||||
EnvironmentKey
|
||||
.map(
|
||||
{ path: String -> File(path) },
|
||||
{ file: File -> file.canonicalPath },
|
||||
)
|
||||
.optional("APPLICATION_PROPERTIES_FILE_PATH")
|
||||
|
||||
/** Bidirectional [String] and [Credentials] mapper. */
|
||||
val credentialsLens: BiDiMapping<String, Credentials> = run {
|
||||
val separator = ":"
|
||||
BiDiMapping(
|
||||
// from String to Credentials
|
||||
{ property ->
|
||||
property.split(separator, limit = 2)
|
||||
.takeIf { it.size == 2 }
|
||||
?.let { (user, pass) -> Credentials(user, pass) }
|
||||
?: throw IllegalArgumentException("Credentials are invalid")
|
||||
},
|
||||
// from Credentials to String
|
||||
{ it.user + separator + it.password }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
//import java.io.File
|
||||
//import org.http4k.cloudnative.env.Environment
|
||||
//import org.http4k.cloudnative.env.EnvironmentKey
|
||||
//import org.http4k.core.Credentials
|
||||
//import org.http4k.lens.BiDiLens
|
||||
//import org.http4k.lens.BiDiMapping
|
||||
//
|
||||
//
|
||||
//internal object Lenses {
|
||||
//
|
||||
// /** The location of this app's env file. */
|
||||
// val APPLICATION_PROPERTIES_FILE_PATH: BiDiLens<Environment, File?> =
|
||||
// EnvironmentKey
|
||||
// .map(
|
||||
// { path: String -> File(path) },
|
||||
// { file: File -> file.canonicalPath },
|
||||
// )
|
||||
// .optional("APPLICATION_PROPERTIES_FILE_PATH")
|
||||
//
|
||||
//
|
||||
// /** Bidirectional [String] and [Credentials] mapper. */
|
||||
// val credentialsLens: BiDiMapping<String, Credentials> = run {
|
||||
// val separator = ":"
|
||||
// BiDiMapping(
|
||||
// // from String to Credentials
|
||||
// { property ->
|
||||
// property.split(separator, limit = 2)
|
||||
// .takeIf { it.size == 2 }
|
||||
// ?.let { (user, pass) -> Credentials(user, pass) }
|
||||
// ?: throw IllegalArgumentException("Credentials are invalid")
|
||||
// },
|
||||
// // from Credentials to String
|
||||
// { it.user + separator + it.password }
|
||||
// )
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -1,74 +1,78 @@
|
|||
package dev.adamko.kafkatorio.processor.config
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import org.http4k.cloudnative.env.Environment
|
||||
import org.http4k.cloudnative.env.MapEnvironment
|
||||
import org.http4k.cloudnative.env.fromYaml
|
||||
import org.http4k.lens.Lens
|
||||
|
||||
|
||||
/**
|
||||
* Load properties from either a `.yaml` or `.env` file.
|
||||
*
|
||||
* (An improved version of [Environment.fromResource])
|
||||
*/
|
||||
fun Environment.Companion.fromResourcesFile(
|
||||
resource: String,
|
||||
default: Environment = EMPTY
|
||||
): Environment {
|
||||
return try {
|
||||
val file =
|
||||
this::class.java
|
||||
.getResource("/${resource.removePrefix("/")}")
|
||||
?.file
|
||||
?.let { File(it) }
|
||||
fromFile(file, default)
|
||||
} catch (e: FileNotFoundException) {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try fetching a properties file the location provided by the
|
||||
* [`JVM`][jvmProperties] or [`System`][env] environment variable
|
||||
* [Lenses.APPLICATION_PROPERTIES_FILE_PATH], if it exists.
|
||||
*
|
||||
* If it does not exist, return [default].
|
||||
*/
|
||||
fun Environment.Companion.fromEnvVarPath(
|
||||
location: Lens<Environment, File?> = Lenses.APPLICATION_PROPERTIES_FILE_PATH,
|
||||
default: Environment = EMPTY,
|
||||
): Environment {
|
||||
val file = (jvmProperties() overrides env())[location]
|
||||
|
||||
return fromFile(file, default)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load properties from either a `.yaml` or `.env` file.
|
||||
*
|
||||
* (An improved version of [Environment.from])
|
||||
*/
|
||||
internal fun Environment.Companion.fromFile(
|
||||
file: File?,
|
||||
default: Environment = EMPTY,
|
||||
): Environment {
|
||||
return file
|
||||
?.takeIf { it.isFile }
|
||||
.let {
|
||||
when (it?.extension) {
|
||||
"yml", "yaml" -> fromYaml(it)
|
||||
"env" -> from(it)
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The same as [Environment.ENV], but refreshes on each invocation (which is useful for testing). */
|
||||
fun Environment.Companion.env(): Environment =
|
||||
MapEnvironment.from(System.getenv().toProperties())
|
||||
|
||||
/** The same as [Environment.JVM_PROPERTIES], but refreshes on each invocation (which is useful for testing). */
|
||||
internal fun Environment.Companion.jvmProperties(): Environment =
|
||||
MapEnvironment.from(System.getProperties())
|
||||
//import java.io.File
|
||||
//import java.io.FileNotFoundException
|
||||
//import org.http4k.cloudnative.env.Environment
|
||||
//import org.http4k.cloudnative.env.MapEnvironment
|
||||
//import org.http4k.cloudnative.env.fromYaml
|
||||
//import org.http4k.lens.Lens
|
||||
//
|
||||
//
|
||||
///**
|
||||
// * Load properties from either a `.yaml` or `.env` file.
|
||||
// *
|
||||
// * (An improved version of [Environment.fromResource])
|
||||
// */
|
||||
//fun Environment.Companion.fromResourcesFile(
|
||||
// resource: String,
|
||||
// default: Environment = EMPTY
|
||||
//): Environment {
|
||||
// return try {
|
||||
// val file =
|
||||
// this::class.java
|
||||
// .getResource("/${resource.removePrefix("/")}")
|
||||
// ?.file
|
||||
// ?.let { File(it) }
|
||||
// fromFile(file, default)
|
||||
// } catch (e: FileNotFoundException) {
|
||||
// default
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
///**
|
||||
// * Try fetching a properties file the location provided by the
|
||||
// * [`JVM`][jvmProperties] or [`System`][env] environment variable
|
||||
// * [Lenses.APPLICATION_PROPERTIES_FILE_PATH], if it exists.
|
||||
// *
|
||||
// * If it does not exist, return [default].
|
||||
// */
|
||||
//fun Environment.Companion.fromEnvVarPath(
|
||||
// location: Lens<Environment, File?> = Lenses.APPLICATION_PROPERTIES_FILE_PATH,
|
||||
// default: Environment = EMPTY,
|
||||
//): Environment {
|
||||
// val file = (jvmProperties() overrides env())[location]
|
||||
//
|
||||
// return fromFile(file, default)
|
||||
//}
|
||||
//
|
||||
//
|
||||
///**
|
||||
// * Load properties from either a `.yaml` or `.env` file.
|
||||
// *
|
||||
// * (An improved version of [Environment.from])
|
||||
// */
|
||||
//internal fun Environment.Companion.fromFile(
|
||||
// file: File?,
|
||||
// default: Environment = EMPTY,
|
||||
//): Environment {
|
||||
// return file
|
||||
// ?.takeIf { it.isFile }
|
||||
// .let {
|
||||
// when (it?.extension) {
|
||||
// "yml", "yaml" -> fromYaml(it)
|
||||
// "env" -> from(it)
|
||||
// else -> default
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//
|
||||
///** The same as [Environment.ENV], but refreshes on each invocation (which is useful for testing). */
|
||||
//fun Environment.Companion.env(): Environment =
|
||||
// MapEnvironment.from(System.getenv().toProperties())
|
||||
//
|
||||
//
|
||||
///** The same as [Environment.JVM_PROPERTIES], but refreshes on each invocation (which is useful for testing). */
|
||||
//internal fun Environment.Companion.jvmProperties(): Environment =
|
||||
// MapEnvironment.from(System.getProperties())
|
||||
|
|
|
|||
|
|
@ -2,33 +2,57 @@ package dev.adamko.kafkatorio.processor
|
|||
|
||||
|
||||
import dev.adamko.kafkatorio.processor.admin.KafkatorioKafkaAdmin
|
||||
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
|
||||
import dev.adamko.kafkatorio.processor.tileserver.WebMapTileServer
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.http4k.server.PolyHandler
|
||||
import org.http4k.server.Undertow
|
||||
import org.http4k.server.asServer
|
||||
import dev.adamko.kafkatorio.processor.tileserver.WebsocketServer
|
||||
import dev.adamko.kafkatorio.processor.tileserver.webServer
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.http4k.server.Http4kServer
|
||||
|
||||
|
||||
fun main(): Unit = runBlocking {
|
||||
suspend fun main() {
|
||||
|
||||
KafkatorioKafkaAdmin.createKafkatorioTopics()
|
||||
val appProps = ApplicationProperties.load()
|
||||
|
||||
val admin = KafkatorioKafkaAdmin(appProps)
|
||||
admin.createKafkatorioTopics()
|
||||
|
||||
val wsServer = WebsocketServer()
|
||||
val tileServer = WebMapTileServer()
|
||||
val tileServer = WebMapTileServer(appProps)
|
||||
|
||||
val topology = KafkatorioTopology(wsServer)
|
||||
topology.start()
|
||||
coroutineScope {
|
||||
launch {
|
||||
val topology = KafkatorioTopology(wsServer, appProps)
|
||||
topology.start()
|
||||
|
||||
val webServer = PolyHandler(
|
||||
tileServer.build(),
|
||||
wsServer.build(),
|
||||
).asServer(Undertow(9073))
|
||||
println("launched KafkatorioTopology")
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
webServer.stop()
|
||||
})
|
||||
// launch {
|
||||
webServer.start().block()
|
||||
// }
|
||||
awaitCancellation()
|
||||
}
|
||||
|
||||
launch {
|
||||
val webServer: Http4kServer = webServer(tileServer, wsServer, appProps)
|
||||
|
||||
currentCoroutineContext().job.invokeOnCompletion {
|
||||
println("webserver stopping. Cause: $it")
|
||||
webServer.stop()
|
||||
}
|
||||
|
||||
webServer.start()
|
||||
println("launched webserver port:${webServer.port()}")
|
||||
|
||||
awaitCancellation()
|
||||
}
|
||||
}
|
||||
}
|
||||
// Runtime.getRuntime().addShutdownHook(Thread {
|
||||
// webServer.stop()
|
||||
// })
|
||||
// launch {
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package dev.adamko.kafkatorio.processor.misc
|
||||
|
||||
import dev.adamko.kafkatorio.processor.KafkatorioTopology
|
||||
import java.io.File
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
|
|
@ -56,7 +55,6 @@ class FileEventsFlow private constructor(
|
|||
|
||||
override val coroutineContext: CoroutineContext =
|
||||
CoroutineName("FileEventsFlow") +
|
||||
KafkatorioTopology.rootJob +
|
||||
Dispatchers.Unconfined +
|
||||
CoroutineExceptionHandler { _, e ->
|
||||
println("handling exception in FileEventsFlow")
|
||||
|
|
|
|||
|
|
@ -18,18 +18,17 @@ import org.http4k.routing.routes
|
|||
import org.http4k.routing.static
|
||||
|
||||
internal class WebMapTileServer(
|
||||
appProps: ApplicationProperties = ApplicationProperties()
|
||||
appProps: ApplicationProperties
|
||||
) {
|
||||
|
||||
private val tilesLoader = ResourceLoader.Directory(appProps.webmapTileDir.absolutePathString())
|
||||
private val tilesLoader = ResourceLoader.Directory(appProps.tileDir.toFile().canonicalPath)
|
||||
|
||||
private val filters = listOf(
|
||||
ETagChecker()(),
|
||||
ETagChecker(),
|
||||
).reduce(Filter::then)
|
||||
|
||||
private val routes: RoutingHttpHandler = routes(
|
||||
"/tiles" bind GET to
|
||||
static(tilesLoader).withFilter(filters)
|
||||
"/tiles" bind GET to static(tilesLoader).withFilter(filters)
|
||||
)
|
||||
|
||||
fun build(): HttpHandler {
|
||||
|
|
@ -37,87 +36,40 @@ internal class WebMapTileServer(
|
|||
}
|
||||
|
||||
|
||||
inner class ETagChecker
|
||||
// : CoroutineScope
|
||||
{
|
||||
private class ETagChecker : Filter {
|
||||
|
||||
// private val tagState: MutableStateFlow<MutableMap<String, String>> =
|
||||
// MutableStateFlow(mutableMapOf())
|
||||
//
|
||||
// override val coroutineContext: CoroutineContext =
|
||||
// CoroutineName("ETagChecker") +
|
||||
// Dispatchers.Unconfined +
|
||||
// KafkatorioTopology.rootJob
|
||||
override fun invoke(next: HttpHandler): HttpHandler = { request: Request ->
|
||||
val requestedETag = request.header("If-None-Match")
|
||||
|
||||
// init {
|
||||
// launch {
|
||||
// appProps.webmapTileDir
|
||||
// .fileEventsFlow()
|
||||
// .filterIsInstance<KWatchEvent.PathEvent>()
|
||||
// .filter { it.path.isRegularFile() }
|
||||
//// .filter { it.path.extension == "png" }
|
||||
// .runningFold(mutableMapOf<String, String>()) { tags, event ->
|
||||
//// println("[WebMapTileServer] handling file update ${event.path}")
|
||||
//
|
||||
// val relativePath = event.path
|
||||
// .relativeTo(appProps.webmapTileDir)
|
||||
// .invariantSeparatorsPathString
|
||||
//
|
||||
// when (event) {
|
||||
// is KWatchEvent.Deleted -> tags.remove(relativePath)
|
||||
// is KWatchEvent.Created,
|
||||
// is KWatchEvent.Modified -> {
|
||||
// val md5 = event.path.md5()
|
||||
// if (md5 != null) {
|
||||
// tags[relativePath] = md5
|
||||
// } else {
|
||||
// println("null md5 for ${event.path}")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// tags
|
||||
// }.cancellable()
|
||||
// .collect(tagState)
|
||||
// }
|
||||
// }
|
||||
var response = next(request)
|
||||
val responseBytes = response.body.stream.use { it.readAllBytes() }
|
||||
val currentETag = responseBytes.md5()
|
||||
|
||||
response = response.body(MemoryBody(responseBytes))
|
||||
|
||||
private val Request.etagKey: String get() = uri.path.removePrefix("/tiles/")
|
||||
|
||||
|
||||
operator fun invoke() = Filter { next ->
|
||||
{ request ->
|
||||
val requestedETag = request.header("If-None-Match")
|
||||
|
||||
var response = next(request)
|
||||
val responseBytes = response.body.stream.use { it.readAllBytes() }
|
||||
val currentETag = responseBytes.md5()
|
||||
|
||||
response = response.body(MemoryBody(responseBytes))
|
||||
|
||||
when {
|
||||
currentETag == null -> {
|
||||
when {
|
||||
currentETag == null -> {
|
||||
// println("[${request.etagKey}] currentETag is null (requested: $requestedETag), executing request")
|
||||
response
|
||||
.header("Cache-Control", "no-cache, max-age=0, must-revalidate")
|
||||
}
|
||||
requestedETag == currentETag -> {
|
||||
response
|
||||
.header("Cache-Control", "no-cache, max-age=0, must-revalidate")
|
||||
}
|
||||
requestedETag == currentETag -> {
|
||||
// println("[${request.etagKey}] currentETag == requested, returning 304 $requestedETag")
|
||||
Response(Status.NOT_MODIFIED)
|
||||
.header("Cache-Control", "no-cache, max-age=0, must-revalidate")
|
||||
}
|
||||
else -> {
|
||||
Response(Status.NOT_MODIFIED)
|
||||
.header("Cache-Control", "no-cache, max-age=0, must-revalidate")
|
||||
}
|
||||
else -> {
|
||||
// println("[${request.etagKey}] currentETag != requested, executing request and adding currentETag:$currentETag (requested:$requestedETag)")
|
||||
response
|
||||
.header("ETag", currentETag)
|
||||
response
|
||||
.header("ETag", currentETag)
|
||||
// .header("Cache-Control", "no-store")
|
||||
.header("Cache-Control", "no-cache, max-age=0, must-revalidate")
|
||||
}
|
||||
.header("Cache-Control", "no-cache, max-age=0, must-revalidate")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun ByteArray.md5(): String? {
|
||||
return if (isEmpty()) {
|
||||
null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package dev.adamko.kafkatorio.processor.tileserver
|
||||
|
||||
import dev.adamko.kafkatorio.processor.config.ApplicationProperties
|
||||
import org.http4k.server.Http4kServer
|
||||
import org.http4k.server.PolyHandler
|
||||
import org.http4k.server.Undertow
|
||||
import org.http4k.server.asServer
|
||||
|
||||
internal suspend fun webServer(
|
||||
tileServer: WebMapTileServer,
|
||||
wsServer: WebsocketServer,
|
||||
appProps: ApplicationProperties,
|
||||
): Http4kServer = PolyHandler(
|
||||
tileServer.build(),
|
||||
wsServer.build(),
|
||||
).asServer(Undertow(appProps.webPort()))
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
package dev.adamko.kafkatorio.processor.tileserver
|
||||
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.TimeSource
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.withIndex
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.http4k.routing.bind
|
||||
import org.http4k.routing.websockets
|
||||
import org.http4k.websocket.Websocket
|
||||
import org.http4k.websocket.WsHandler
|
||||
import org.http4k.websocket.WsMessage
|
||||
import org.http4k.websocket.WsStatus
|
||||
|
||||
|
||||
class WebsocketServer {
|
||||
|
||||
private val serverScope = CoroutineScope(
|
||||
CoroutineName("WsServer") +
|
||||
Dispatchers.IO +
|
||||
SupervisorJob() +
|
||||
CoroutineExceptionHandler { _, e ->
|
||||
println("[ws.WsServer] handling exception ${e.message}")
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
private val clients: MutableSharedFlow<Websocket> = MutableSharedFlow(
|
||||
replay = 100,
|
||||
extraBufferCapacity = 100,
|
||||
BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
|
||||
private val messages: MutableSharedFlow<String> = MutableSharedFlow(
|
||||
replay = 100,
|
||||
extraBufferCapacity = 100,
|
||||
BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
|
||||
private fun bindClient(ws: Websocket) = serverScope.launch {
|
||||
println("[ws.bindClient] ${ws.upgradeRequest.toMessage()}")
|
||||
clients.emit(ws)
|
||||
}
|
||||
|
||||
|
||||
fun sendMessage(msg: String) = serverScope.launch {
|
||||
println("[ws.sendMessage] $msg")
|
||||
messages.emit(msg)
|
||||
}
|
||||
|
||||
|
||||
fun build(): WsHandler {
|
||||
val startTime = TimeSource.Monotonic.markNow()
|
||||
|
||||
serverScope.launch {
|
||||
supervisorScope {
|
||||
clients
|
||||
.withIndex()
|
||||
.onEach { (id, ws) ->
|
||||
createClient(id, ws)
|
||||
}.launchIn(serverScope)
|
||||
}
|
||||
}
|
||||
|
||||
serverScope.launch {
|
||||
while (isActive) {
|
||||
delay(10.seconds)
|
||||
sendMessage("elapsed ${startTime.elapsedNow()}")
|
||||
}
|
||||
}
|
||||
|
||||
return websockets(
|
||||
"/ws/foo" bind { ws: Websocket ->
|
||||
serverScope.launch { bindClient(ws) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun createClient(id: Int, ws: Websocket) {
|
||||
val clientJob = SupervisorJob(serverScope.coroutineContext.job)
|
||||
|
||||
serverScope.launch(CoroutineName("ws-client-$id") + clientJob) {
|
||||
// return serverScope.launch(CoroutineName("ws-client-$id")) {
|
||||
// supervisorScope {
|
||||
launch {
|
||||
|
||||
suspendCancellableCoroutine<WsStatus> { cont ->
|
||||
println("[ws-client] entering suspendCancellableCoroutine")
|
||||
|
||||
cont.invokeOnCancellation { e: Throwable? ->
|
||||
launch { sendMessage("[ws-client.invokeOnCompletion] closing client ws ${e?.message}") }
|
||||
when (e) {
|
||||
null -> ws.close(WsStatus.NORMAL)
|
||||
else -> ws.close(WsStatus.ABNORMAL_CLOSE)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onError { e: Throwable ->
|
||||
launch { sendMessage("[ws-client.onError] client $id error [${e.message}]") }
|
||||
if (!cont.isCompleted) {
|
||||
cont.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onClose { status: WsStatus ->
|
||||
launch { sendMessage("[ws-client.onClose] client $id disconnected [${status}]") }
|
||||
if (!cont.isCompleted) {
|
||||
cont.resume(status)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onMessage { wsMsg ->
|
||||
launch { sendMessage("$id says: ${wsMsg.bodyString()}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println("[ws-client] $id listening to messages")
|
||||
messages
|
||||
.onEach { msg ->
|
||||
println("[ws-client] sending message to $id")
|
||||
ws.send(WsMessage(msg))
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@ import dev.adamko.kafkatorio.processor.admin.topicName
|
|||
import dev.adamko.kafkatorio.processor.serdes.jsonMapper
|
||||
import dev.adamko.kafkatorio.schema.common.FactorioServerId
|
||||
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket
|
||||
import dev.adamko.kotka.extensions.component1
|
||||
import dev.adamko.kotka.extensions.component2
|
||||
import dev.adamko.kotka.extensions.consumedAs
|
||||
import dev.adamko.kotka.extensions.producedAs
|
||||
import dev.adamko.kotka.extensions.streams.map
|
||||
|
|
@ -15,7 +17,6 @@ import kotlinx.serialization.decodeFromString
|
|||
import org.apache.kafka.common.serialization.Serdes
|
||||
import org.apache.kafka.streams.StreamsBuilder
|
||||
import org.apache.kafka.streams.kstream.KStream
|
||||
import org.apache.kafka.streams.processor.RecordContext
|
||||
|
||||
|
||||
fun factorioServerPacketStream(
|
||||
|
|
@ -46,7 +47,7 @@ fun splitFactorioServerPacketStream(
|
|||
jsonMapper.serde(),
|
||||
jsonMapper.serde(),
|
||||
)
|
||||
) { _: FactorioServerId, value: KafkatorioPacket, _: RecordContext ->
|
||||
) { (_: FactorioServerId, value: KafkatorioPacket) ->
|
||||
// println("[$key] sending event:${value.eventType} to topic:${value.data.objectName()}")
|
||||
value.data.topicName
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
package dev.adamko.kafkatorio.processor.topology
|
||||
|
||||
import dev.adamko.kafkatorio.processor.WebsocketServer
|
||||
import dev.adamko.kafkatorio.processor.admin.TOPIC_BROADCAST_TO_WEBSOCKET
|
||||
import dev.adamko.kafkatorio.processor.admin.topicName
|
||||
import dev.adamko.kafkatorio.processor.tileserver.WebsocketServer
|
||||
import dev.adamko.kafkatorio.schema.common.FactorioServerId
|
||||
import dev.adamko.kafkatorio.schema.jsonMapperKafkatorio
|
||||
import dev.adamko.kafkatorio.schema.packets.EventServerPacket
|
||||
import dev.adamko.kafkatorio.schema.packets.KafkatorioPacket
|
||||
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
|
||||
import dev.adamko.kafkatorio.schema.packets.SurfaceUpdate
|
||||
import dev.adamko.kotka.extensions.consumedAs
|
||||
import dev.adamko.kotka.extensions.producedAs
|
||||
import dev.adamko.kotka.extensions.stream
|
||||
import dev.adamko.kotka.extensions.streams.forEach
|
||||
import dev.adamko.kotka.extensions.streams.map
|
||||
import dev.adamko.kotka.extensions.streams.to
|
||||
import dev.adamko.kotka.kxs.serde
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.apache.kafka.streams.StreamsBuilder
|
||||
|
|
@ -32,7 +36,7 @@ fun broadcastToWebsocket(
|
|||
builder: StreamsBuilder = StreamsBuilder(),
|
||||
) {
|
||||
|
||||
builder.preparePlayerUpdatesBroadcast()
|
||||
builder.broadcastKafkatorioPackets()
|
||||
|
||||
builder.stream<FactorioServerId, EventServerPacket>(
|
||||
TOPIC_BROADCAST_TO_WEBSOCKET,
|
||||
|
|
@ -40,25 +44,54 @@ fun broadcastToWebsocket(
|
|||
).forEach(
|
||||
"$pid.websocket-server.send-message"
|
||||
) { _, esPacket ->
|
||||
websocketServer.sendMessage(jsonMapper.encodeToString(esPacket))
|
||||
runBlocking {
|
||||
websocketServer.sendMessage(jsonMapper.encodeToString(esPacket))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun StreamsBuilder.preparePlayerUpdatesBroadcast() {
|
||||
stream<FactorioServerId, KafkatorioPacket>(
|
||||
private fun StreamsBuilder.broadcastKafkatorioPackets() {
|
||||
val playerUpdatesStream = stream(
|
||||
consumedAs(
|
||||
"$pid.player-updates",
|
||||
jsonMapper.serde(FactorioServerId.serializer()),
|
||||
jsonMapper.serde(KafkatorioPacket.serializer()),
|
||||
),
|
||||
PlayerUpdate::class.topicName,
|
||||
consumedAs("$pid.player-updates", jsonMapper.serde(), jsonMapper.serde()),
|
||||
).map<FactorioServerId, KafkatorioPacket, FactorioServerId, EventServerPacket>(
|
||||
"$pid.player-updates.convert"
|
||||
) { serverId, playerPacket ->
|
||||
serverId to EventServerPacket.Kafkatorio(playerPacket)
|
||||
SurfaceUpdate::class.topicName,
|
||||
)
|
||||
|
||||
playerUpdatesStream.map<FactorioServerId, KafkatorioPacket, FactorioServerId, EventServerPacket>(
|
||||
"$pid.packet-updates.convert"
|
||||
) { serverId, packet ->
|
||||
serverId to EventServerPacket.Kafkatorio(packet)
|
||||
}.to(
|
||||
producedAs(
|
||||
"$pid.event-server-packets.produce",
|
||||
jsonMapper.serde(FactorioServerId.serializer()),
|
||||
jsonMapper.serde(EventServerPacket.serializer()),
|
||||
)
|
||||
) { TOPIC_BROADCAST_TO_WEBSOCKET }
|
||||
}
|
||||
|
||||
|
||||
private fun StreamsBuilder.prepareServerUpdateBroadcast() {
|
||||
|
||||
val surfaceUpdatesStream = stream<FactorioServerId, KafkatorioPacket>(
|
||||
SurfaceUpdate::class.topicName,
|
||||
consumedAs("$pid.surface-updates", jsonMapper.serde(), jsonMapper.serde()),
|
||||
)
|
||||
|
||||
surfaceUpdatesStream.map<FactorioServerId, KafkatorioPacket, FactorioServerId, EventServerPacket>(
|
||||
"$pid.surface-updates.convert"
|
||||
) { serverId, serverUpdatePacket ->
|
||||
serverId to EventServerPacket.Kafkatorio(serverUpdatePacket)
|
||||
}.to(
|
||||
TOPIC_BROADCAST_TO_WEBSOCKET,
|
||||
producedAs(
|
||||
"$pid.player-updates.stream",
|
||||
jsonMapper.serde(FactorioServerId.serializer()),
|
||||
jsonMapper.serde(EventServerPacket.serializer()),
|
||||
)
|
||||
)
|
||||
) { TOPIC_BROADCAST_TO_WEBSOCKET }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
kafkatorio-web-map/**
|
||||
Loading…
Add table
Add a link
Reference in a new issue