Compare commits
No commits in common. "d124e2a26f1f54bcf9966ebf469d5793129fd3d8" and "e044203efc7356972587fc052677d7ef88683158" have entirely different histories.
d124e2a26f
...
e044203efc
4 changed files with 39 additions and 146 deletions
|
@ -2,7 +2,6 @@ package de.kif.common
|
||||||
|
|
||||||
import com.soywiz.klock.*
|
import com.soywiz.klock.*
|
||||||
import com.soywiz.klock.locale.german
|
import com.soywiz.klock.locale.german
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
fun formatDateTime(unix: Long, timezone: Long) =
|
fun formatDateTime(unix: Long, timezone: Long) =
|
||||||
DateFormat("EEEE, d. MMMM y HH:mm")
|
DateFormat("EEEE, d. MMMM y HH:mm")
|
||||||
|
@ -25,8 +24,10 @@ fun formatTime(unix: Long, timezone: Long) =
|
||||||
.format(DateTimeTz.utc(DateTime(unix), TimezoneOffset(timezone.toDouble())))
|
.format(DateTimeTz.utc(DateTime(unix), TimezoneOffset(timezone.toDouble())))
|
||||||
|
|
||||||
fun formatTimeDiff(time: Long, now: Long, timezone: Long): String {
|
fun formatTimeDiff(time: Long, now: Long, timezone: Long): String {
|
||||||
var dt = ceil(((time - now) / 1000) / 60.0).toLong()
|
var dt = (time - now) / 1000
|
||||||
|
|
||||||
|
val seconds = dt % 60
|
||||||
|
dt /= 60
|
||||||
val minutes = dt % 60
|
val minutes = dt % 60
|
||||||
dt /= 60
|
dt /= 60
|
||||||
val hours = dt % 24
|
val hours = dt % 24
|
||||||
|
@ -34,8 +35,12 @@ fun formatTimeDiff(time: Long, now: Long, timezone: Long): String {
|
||||||
val days = dt
|
val days = dt
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
days > 1L -> "in $days Tagen"
|
days > 1L -> {
|
||||||
days > 0L -> "morgen"
|
"in $days Tagen"
|
||||||
|
}
|
||||||
|
days > 0L -> {
|
||||||
|
"morgen"
|
||||||
|
}
|
||||||
hours > 1L -> {
|
hours > 1L -> {
|
||||||
val nowHour = DateFormat("HH")
|
val nowHour = DateFormat("HH")
|
||||||
.withLocale(KlockLocale.german)
|
.withLocale(KlockLocale.german)
|
||||||
|
@ -49,11 +54,18 @@ fun formatTimeDiff(time: Long, now: Long, timezone: Long): String {
|
||||||
"morgen"
|
"morgen"
|
||||||
} else "um $ht"
|
} else "um $ht"
|
||||||
}
|
}
|
||||||
hours > 0L -> "in " + hours.toTimeString() + ":" + minutes.toTimeString()
|
hours > 0L -> {
|
||||||
minutes > 1L -> "in 00:" + minutes.toTimeString()
|
"in " + hours.toString().padStart(2, '0') + ":" + (minutes + if (seconds > 0) 1 else 0).toString().padStart(
|
||||||
else -> "in > 1 Minute"
|
2,
|
||||||
|
'0'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
minutes > 0L -> {
|
||||||
|
"in 00:" + (minutes + if (seconds > 0) 1 else 0).toString().padStart(2, '0')
|
||||||
|
}
|
||||||
|
seconds > 0L -> {
|
||||||
|
"in > 1 Minute"
|
||||||
|
}
|
||||||
|
else -> "---"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
private inline fun Number.toTimeString() = toString().padStart(2, '0')
|
|
||||||
|
|
|
@ -3,15 +3,11 @@ package de.kif.frontend
|
||||||
import de.kif.common.MessageBox
|
import de.kif.common.MessageBox
|
||||||
import de.kif.common.MessageType
|
import de.kif.common.MessageType
|
||||||
import de.kif.common.RepositoryType
|
import de.kif.common.RepositoryType
|
||||||
import de.kif.common.Serialization
|
|
||||||
import de.kif.frontend.repository.*
|
import de.kif.frontend.repository.*
|
||||||
import de.westermann.kwebview.clearInterval
|
import de.westermann.kwebview.clearInterval
|
||||||
import de.westermann.kwebview.createHtmlView
|
import de.westermann.kwebview.createHtmlView
|
||||||
import de.westermann.kwebview.interval
|
import de.westermann.kwebview.interval
|
||||||
import kotlinx.serialization.DynamicObjectParser
|
import kotlinx.serialization.DynamicObjectParser
|
||||||
import org.w3c.dom.EventSource
|
|
||||||
import org.w3c.dom.MessageEvent
|
|
||||||
import org.w3c.dom.events.EventListener
|
|
||||||
import org.w3c.dom.get
|
import org.w3c.dom.get
|
||||||
import org.w3c.xhr.XMLHttpRequest
|
import org.w3c.xhr.XMLHttpRequest
|
||||||
import kotlin.browser.document
|
import kotlin.browser.document
|
||||||
|
@ -19,8 +15,7 @@ import kotlin.browser.window
|
||||||
|
|
||||||
class PushServiceClient {
|
class PushServiceClient {
|
||||||
private val prefix = js("prefix")
|
private val prefix = js("prefix")
|
||||||
private val pollingUrl = "$prefix/api/updates"
|
private val url = "$prefix/api/updates"
|
||||||
private val eventUrl = "$prefix/api/events"
|
|
||||||
private val body = document.body ?: createHtmlView()
|
private val body = document.body ?: createHtmlView()
|
||||||
private val parser = DynamicObjectParser()
|
private val parser = DynamicObjectParser()
|
||||||
|
|
||||||
|
@ -60,17 +55,16 @@ class PushServiceClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onError(code: Int): Boolean {
|
private fun onError(code: Int) {
|
||||||
if (errorTimeout > 0) {
|
if (errorTimeout > 0) {
|
||||||
errorTimeout--
|
errorTimeout--
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!body.classList.contains("offline")) {
|
if (!body.classList.contains("offline")) {
|
||||||
console.log("Offline reason: $code")
|
console.log("Offline reason: $code")
|
||||||
}
|
}
|
||||||
body.classList.add("offline")
|
body.classList.add("offline")
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun request() {
|
private fun request() {
|
||||||
|
@ -88,57 +82,21 @@ class PushServiceClient {
|
||||||
} else {
|
} else {
|
||||||
onError(-1)
|
onError(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
onError(xmlHttpRequest.status.toInt())
|
onError(xmlHttpRequest.status.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Unit
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
onError(-2)
|
onError(-2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
xmlHttpRequest.open("GET", "$pollingUrl?timestamp=$timestamp", true)
|
xmlHttpRequest.open("GET", "$url?timestamp=$timestamp", true)
|
||||||
xmlHttpRequest.overrideMimeType("application/json")
|
xmlHttpRequest.overrideMimeType("application/json")
|
||||||
xmlHttpRequest.send()
|
xmlHttpRequest.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initEventSource() {
|
|
||||||
val eventSource = EventSource(eventUrl)
|
|
||||||
var timeout = 3
|
|
||||||
|
|
||||||
eventSource.addEventListener("update", EventListener {
|
|
||||||
val event = it as? MessageEvent ?: return@EventListener
|
|
||||||
val message = event.data as? String ?: return@EventListener
|
|
||||||
onMessage(Serialization.parse(MessageBox.serializer(), message))
|
|
||||||
})
|
|
||||||
eventSource.addEventListener("ping", EventListener {
|
|
||||||
timeout = 3
|
|
||||||
val event = it as? MessageEvent ?: return@EventListener
|
|
||||||
val s = event.data as? String ?: return@EventListener
|
|
||||||
if (s != signature) {
|
|
||||||
reload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
intervalId = interval(500) {
|
|
||||||
timeout -= 1
|
|
||||||
if (timeout <= 0) {
|
|
||||||
if (onError(-1)) {
|
|
||||||
val id = intervalId
|
|
||||||
if (id != null) {
|
|
||||||
clearInterval(id)
|
|
||||||
intervalId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
intervalId = interval(500) {
|
|
||||||
request()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val messageHandlers: List<MessageHandler> = listOf(
|
private val messageHandlers: List<MessageHandler> = listOf(
|
||||||
RoomRepository.handler,
|
RoomRepository.handler,
|
||||||
ScheduleRepository.handler,
|
ScheduleRepository.handler,
|
||||||
|
@ -150,19 +108,8 @@ class PushServiceClient {
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
try {
|
intervalId = interval(500) {
|
||||||
initEventSource()
|
request()
|
||||||
} catch (e: Exception) {
|
|
||||||
console.log("Cannot connect to event source, use polling fallback!")
|
|
||||||
val id = intervalId
|
|
||||||
if (id != null) {
|
|
||||||
clearInterval(id)
|
|
||||||
intervalId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
intervalId = interval(500) {
|
|
||||||
request()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,21 +3,18 @@ package de.kif.backend.util
|
||||||
import de.kif.backend.repository.*
|
import de.kif.backend.repository.*
|
||||||
import de.kif.backend.route.api.error
|
import de.kif.backend.route.api.error
|
||||||
import de.kif.backend.route.api.success
|
import de.kif.backend.route.api.success
|
||||||
import de.kif.common.*
|
import de.kif.common.Message
|
||||||
|
import de.kif.common.MessageBox
|
||||||
|
import de.kif.common.MessageType
|
||||||
|
import de.kif.common.RepositoryType
|
||||||
import de.kif.common.model.Post
|
import de.kif.common.model.Post
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.http.CacheControl
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.response.cacheControl
|
|
||||||
import io.ktor.response.respondTextWriter
|
|
||||||
import io.ktor.routing.Route
|
import io.ktor.routing.Route
|
||||||
import io.ktor.routing.get
|
import io.ktor.routing.get
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.channels.broadcast
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.html.currentTimeMillis
|
import kotlinx.html.currentTimeMillis
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
object PushService {
|
object PushService {
|
||||||
|
@ -27,30 +24,16 @@ object PushService {
|
||||||
|
|
||||||
private val messages: MutableList<Pair<Long, Message>> = mutableListOf()
|
private val messages: MutableList<Pair<Long, Message>> = mutableListOf()
|
||||||
|
|
||||||
private val channel = Channel<SSE>()
|
|
||||||
val broadcast = channel.broadcast()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the message with the current timestamp
|
* Save the message with the current timestamp
|
||||||
*/
|
*/
|
||||||
suspend fun notify(type: MessageType, repository: RepositoryType, id: Long) {
|
fun notify(type: MessageType, repository: RepositoryType, id: Long) {
|
||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
val message = Message(type, repository, id)
|
val message = Message(type, repository, id)
|
||||||
|
|
||||||
synchronized(messages) {
|
synchronized(messages) {
|
||||||
messages += Pair(timestamp, message)
|
messages += Pair(timestamp, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.send(
|
|
||||||
SSE.Message(
|
|
||||||
MessageBox(
|
|
||||||
System.currentTimeMillis(),
|
|
||||||
signature,
|
|
||||||
true,
|
|
||||||
listOf(message)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getIndexOfTimestamp(timestamp: Long): Int {
|
private fun getIndexOfTimestamp(timestamp: Long): Int {
|
||||||
|
@ -110,17 +93,6 @@ object PushService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ping() {
|
|
||||||
runBlocking {
|
|
||||||
channel.send(SSE.Ping)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class SSE {
|
|
||||||
class Message(val messageBox: MessageBox) : SSE()
|
|
||||||
object Ping: SSE()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Route.pushService() {
|
fun Route.pushService() {
|
||||||
|
@ -132,42 +104,12 @@ fun Route.pushService() {
|
||||||
|
|
||||||
val messageBox = PushService.getMessages(timestamp)
|
val messageBox = PushService.getMessages(timestamp)
|
||||||
|
|
||||||
call.response.cacheControl(CacheControl.NoCache(null))
|
|
||||||
call.success(messageBox)
|
call.success(messageBox)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
call.error(HttpStatusCode.InternalServerError)
|
call.error(HttpStatusCode.InternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get("/api/events") {
|
|
||||||
val events = PushService.broadcast.openSubscription()
|
|
||||||
try {
|
|
||||||
call.response.cacheControl(CacheControl.NoCache(null))
|
|
||||||
call.respondTextWriter(contentType = ContentType.Text.EventStream) {
|
|
||||||
write("retry: 1000\n")
|
|
||||||
write("\n")
|
|
||||||
|
|
||||||
for (event in events) {
|
|
||||||
write("id: 0\n")
|
|
||||||
when (event) {
|
|
||||||
is SSE.Ping -> {
|
|
||||||
write("event: ping\n")
|
|
||||||
write("data: ${PushService.signature}\n")
|
|
||||||
}
|
|
||||||
is SSE.Message -> {
|
|
||||||
write("event: update\n")
|
|
||||||
write("data: ${Serialization.stringify(MessageBox.serializer(), event.messageBox)}\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
write("\n")
|
|
||||||
flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
events.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RoomRepository.registerPushService()
|
RoomRepository.registerPushService()
|
||||||
ScheduleRepository.registerPushService()
|
ScheduleRepository.registerPushService()
|
||||||
TrackRepository.registerPushService()
|
TrackRepository.registerPushService()
|
||||||
|
@ -179,19 +121,11 @@ fun Route.pushService() {
|
||||||
thread(
|
thread(
|
||||||
start = true,
|
start = true,
|
||||||
isDaemon = true,
|
isDaemon = true,
|
||||||
name = "push-service"
|
name = "PushServiceGC"
|
||||||
) {
|
) {
|
||||||
var gc = currentTimeMillis() + 1000 * 60 * 10
|
|
||||||
while (true) {
|
while (true) {
|
||||||
Thread.sleep(500)
|
PushService.gc(currentTimeMillis() - 1000 * 60)
|
||||||
PushService.ping()
|
Thread.sleep(1000 * 60 * 5)
|
||||||
|
|
||||||
val now = currentTimeMillis()
|
|
||||||
if (gc < now) {
|
|
||||||
gc = now + 1000 * 60 * 10
|
|
||||||
PushService.gc(now - 1000 * 60)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ class MainTemplate(
|
||||||
script {
|
script {
|
||||||
unsafe {
|
unsafe {
|
||||||
+"let prefix = '$prefix';\n"
|
+"let prefix = '$prefix';\n"
|
||||||
+"require.config({waitSeconds: 120, baseUrl: '$prefix/static'});\n"
|
+"require.config({waitSeconds: 60, baseUrl: '$prefix/static'});\n"
|
||||||
+"require([${Resources.jsModules}]);\n"
|
+"require([${Resources.jsModules}]);\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue