Skip to content
This repository has been archived by the owner on Aug 10, 2024. It is now read-only.

Commit

Permalink
fix problem with UrlToPathSegmentsRF
Browse files Browse the repository at this point in the history
  • Loading branch information
sanity committed May 22, 2023
1 parent d701edb commit 7660c97
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 26 deletions.
21 changes: 19 additions & 2 deletions api/kweb-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ public final class kweb/ElementKt {
public static synthetic fun new$default (Lkweb/Element;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/lang/Object;
}

public class kweb/FooterElement : kweb/Element {
public fun <init> (Lkweb/Element;)V
}

public class kweb/FormElement : kweb/Element {
public fun <init> (Lkweb/Element;)V
}
Expand Down Expand Up @@ -372,6 +376,10 @@ public final class kweb/LinkRelationship : java/lang/Enum {
public static fun values ()[Lkweb/LinkRelationship;
}

public class kweb/MainElement : kweb/Element {
public fun <init> (Lkweb/Element;)V
}

public class kweb/MetaElement : kweb/Element {
public fun <init> (Lkweb/Element;)V
}
Expand Down Expand Up @@ -407,6 +415,8 @@ public final class kweb/PreludeKt {
public static synthetic fun div$default (Lkweb/ElementCreator;Ljava/util/Map;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkweb/DivElement;
public static final fun fileInput (Lkweb/ElementCreator;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/util/Map;)Lkweb/html/fileUpload/FileFormInput;
public static synthetic fun fileInput$default (Lkweb/ElementCreator;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lkweb/html/fileUpload/FileFormInput;
public static final fun footer (Lkweb/ElementCreator;Ljava/util/Map;Lkotlin/jvm/functions/Function2;)Lkweb/FooterElement;
public static synthetic fun footer$default (Lkweb/ElementCreator;Ljava/util/Map;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkweb/FooterElement;
public static final fun form (Lkweb/ElementCreator;Ljava/util/Map;Lkotlin/jvm/functions/Function2;)Lkweb/FormElement;
public static synthetic fun form$default (Lkweb/ElementCreator;Ljava/util/Map;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkweb/FormElement;
public static final fun get (Lkweb/state/KVar;I)Lkweb/state/KVar;
Expand Down Expand Up @@ -437,6 +447,8 @@ public final class kweb/PreludeKt {
public static synthetic fun li$default (Lkweb/ElementCreator;Ljava/util/Map;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkweb/LIElement;
public static final fun link (Lkweb/ElementCreator;Lkweb/LinkRelationship;Ljava/net/URL;Ljava/lang/String;Ljava/util/Map;)Lkweb/Element;
public static synthetic fun link$default (Lkweb/ElementCreator;Lkweb/LinkRelationship;Ljava/net/URL;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lkweb/Element;
public static final fun main (Lkweb/ElementCreator;Ljava/util/Map;Lkotlin/jvm/functions/Function2;)Lkweb/MainElement;
public static synthetic fun main$default (Lkweb/ElementCreator;Ljava/util/Map;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkweb/MainElement;
public static final fun meta (Lkweb/ElementCreator;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lkweb/MetaElement;
public static synthetic fun meta$default (Lkweb/ElementCreator;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkweb/MetaElement;
public static final fun nav (Lkweb/ElementCreator;Ljava/util/Map;Lkotlin/jvm/functions/Function2;)Lkweb/NavElement;
Expand Down Expand Up @@ -868,6 +880,7 @@ public final class kweb/client/ClientConnection$Caching : kweb/client/ClientConn

public final class kweb/client/ClientConnection$WebSocket : kweb/client/ClientConnection {
public fun <init> (Lio/ktor/websocket/WebSocketSession;)V
public final fun close (Lio/ktor/websocket/CloseReason;)V
public final fun getSendCount ()I
public fun send (Ljava/lang/String;)V
public final fun setSendCount (I)V
Expand Down Expand Up @@ -993,6 +1006,7 @@ public abstract class kweb/config/KwebConfiguration {
public fun <init> ()V
public abstract fun getBuildpageTimeout ()Ljava/time/Duration;
public fun getClientOfflineToastTextTemplate ()Ljava/lang/String;
public abstract fun getClientStateMaxSize ()J
public abstract fun getClientStateStatsEnabled ()Z
public abstract fun getClientStateTimeout ()Ljava/time/Duration;
public fun getHandleFavicon ()Z
Expand All @@ -1009,6 +1023,7 @@ protected final class kweb/config/KwebConfiguration$Accessor {
public class kweb/config/KwebDefaultConfiguration : kweb/config/KwebConfiguration {
public fun <init> ()V
public fun getBuildpageTimeout ()Ljava/time/Duration;
public fun getClientStateMaxSize ()J
public fun getClientStateStatsEnabled ()Z
public fun getClientStateTimeout ()Ljava/time/Duration;
}
Expand Down Expand Up @@ -1780,8 +1795,10 @@ public final class kweb/plugins/staticFiles/ResourceFolder {
}

public final class kweb/plugins/staticFiles/StaticFilesPlugin : kweb/plugins/KwebPlugin {
public fun <init> (Ljava/io/File;Ljava/lang/String;)V
public fun <init> (Lkweb/plugins/staticFiles/ResourceFolder;Ljava/lang/String;)V
public fun <init> (Ljava/io/File;Ljava/lang/String;I)V
public synthetic fun <init> (Ljava/io/File;Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Lkweb/plugins/staticFiles/ResourceFolder;Ljava/lang/String;I)V
public synthetic fun <init> (Lkweb/plugins/staticFiles/ResourceFolder;Ljava/lang/String;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun appServerConfigurator (Lio/ktor/server/routing/Routing;)V
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/kotlin/kweb/ElementCreator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ open class ElementCreator<out PARENT_TYPE : Element>(
val insertBefore: String? = null
) {

companion object : KLogging()
companion object :
KLogging()

@Volatile
private var cleanupListeners: MutableCollection<Cleaner>? = null
Expand Down
44 changes: 31 additions & 13 deletions src/main/kotlin/kweb/Kweb.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ class Kweb private constructor(
*/
val clientState: Cache<String, RemoteClientState> = CacheBuilder.newBuilder()
.expireAfterAccess(kwebConfig.clientStateTimeout)
.maximumSize(kwebConfig.clientStateMaxSize)
.apply { if (kwebConfig.clientStateStatsEnabled) recordStats() }
.removalListener<String, RemoteClientState> { rl ->
rl.value?.triggerCloseListeners()
Expand Down Expand Up @@ -249,8 +250,11 @@ class Kweb private constructor(
}
}

private suspend fun RemoteClientState?.ensureSessionExists(sock: DefaultWebSocketSession, sessionId: String) : RemoteClientState{
if(this == null) {
private suspend fun RemoteClientState?.ensureSessionExists(
sock: DefaultWebSocketSession,
sessionId: String
): RemoteClientState {
if (this == null) {
sock.close(CloseReason(CloseReason.Codes.NOT_CONSISTENT, "Session not found. Please reload"))
error("Unable to find server state corresponding to client id ${sessionId}")
}
Expand All @@ -266,14 +270,28 @@ class Kweb private constructor(

val remoteClientState = clientState.getIfPresent(hello.id).ensureSessionExists(this, hello.id)

assert(remoteClientState.clientConnection is Caching)
logger.debug { "Received message from remoteClient ${remoteClientState.id}, flushing outbound message cache" }
val cachedConnection = remoteClientState.clientConnection as Caching
val webSocketClientConnection = ClientConnection.WebSocket(this)
remoteClientState.clientConnection = webSocketClientConnection
logger.debug { "Set clientConnection for ${remoteClientState.id} to WebSocket, sending ${cachedConnection.size} cached messages" }
cachedConnection.read().forEach { webSocketClientConnection.send(it) }
val currentCC = remoteClientState.clientConnection
remoteClientState.clientConnection = when (currentCC) {
is Caching -> {
val webSocketClientConnection = ClientConnection.WebSocket(this)
currentCC.read().forEach {
webSocketClientConnection.send(it)
}
remoteClientState.addCloseHandler {
webSocketClientConnection.close(CloseReason(4002, "RemoteClientState closed by server, likely due to cache expiry"))
}
webSocketClientConnection
}

is ClientConnection.WebSocket -> {
currentCC.close(CloseReason(4001, "Client reconnected via another connection"))
val ws = ClientConnection.WebSocket(this)
remoteClientState.addCloseHandler {
ws.close(CloseReason(4002, "RemoteClientState closed by server, likely due to cache expiry"))
}
ws
}
}

try {
for (frame in incoming) {
Expand Down Expand Up @@ -301,6 +319,7 @@ class Kweb private constructor(
?: error("No resultHandler for $resultId, for client ${remoteClientState.id}")
resultHandler(result)
}

message.keepalive -> {
logger.debug { "keepalive received from client ${hello.id}" }
}
Expand All @@ -319,13 +338,12 @@ class Kweb private constructor(
}
}

fun determineClientPrefix(call:ApplicationCall) : String{
fun determineClientPrefix(call: ApplicationCall): String {
val kwClientPrefixCookieName = "kwebClientPrefix"
val currentPrefix = call.request.cookies.get(kwClientPrefixCookieName)
return if(currentPrefix != null) {
return if (currentPrefix != null) {
currentPrefix
}
else {
} else {
val newClientPrefix = createNonce(6)
call.response.cookies.append(kwClientPrefixCookieName, newClientPrefix)
newClientPrefix
Expand Down
11 changes: 9 additions & 2 deletions src/main/kotlin/kweb/client/ClientConnection.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package kweb.client

import io.ktor.websocket.Frame
import io.ktor.websocket.*
import io.ktor.websocket.Frame.Text
import io.ktor.websocket.WebSocketSession
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kweb.state.CloseReason
import mu.two.KotlinLogging
import java.util.concurrent.ConcurrentLinkedQueue

Expand Down Expand Up @@ -36,6 +36,13 @@ sealed class ClientConnection {
}
sendCount++
}

fun close(reason : io.ktor.websocket.CloseReason) {
runBlocking {
channel.close(reason)
sendBuffer.close()
}
}
}

class Caching : ClientConnection() {
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/kweb/config/KwebConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import java.util.*
* Please note this is not [Kweb.Feature.Configuration], which is a Ktor specific config block
*/
abstract class KwebConfiguration {

abstract val clientStateMaxSize: Long

private val logger = KotlinLogging.logger {}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/kweb/config/KwebDefaultConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ open class KwebDefaultConfiguration : KwebConfiguration() {
override val clientStateTimeout: Duration =
Accessor.getProperty("kweb.client.state.timeout")?.let { Duration.parse(it) }
?: Duration.ofMinutes(5)

private val estimatedMemoryPerClient = 102_400L // 100KB
override val clientStateMaxSize: Long =
Accessor.getProperty("kweb.client.state.max.size")?.toLongOrNull()
?: (Runtime.getRuntime().maxMemory() / estimatedMemoryPerClient)
}
9 changes: 6 additions & 3 deletions src/main/kotlin/kweb/plugins/staticFiles/StaticFilesPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ import java.io.File
* @property resourceFolder For serving resources, the path to the folder which will be served
* @property servedRoute The route where these assets are being served
*/
class StaticFilesPlugin private constructor(private val servedRoute: String, private val maxCacheAgeSeconds: Int = 60 * 60) : KwebPlugin() {

// TODO: Should set cache age to 0 if Kweb debug param is true - but currently that isn't exposed to plugins

class StaticFilesPlugin private constructor(private val servedRoute: String, private val maxCacheAgeSeconds: Int) : KwebPlugin() {

private lateinit var datasource: (Route) -> Unit

constructor(rootFolder: File, servedRoute: String) : this(servedRoute) {
constructor(rootFolder: File, servedRoute: String, maxCacheAgeSeconds: Int = 60 * 60) : this(servedRoute, maxCacheAgeSeconds) {
datasource = {
it.staticRootFolder = rootFolder
}
}

constructor(resourceFolder: ResourceFolder, servedRoute: String) : this(servedRoute) {
constructor(resourceFolder: ResourceFolder, servedRoute: String, maxCacheAgeSeconds: Int = 60 * 60) : this(servedRoute, maxCacheAgeSeconds) {
datasource = {
it.resources(resourceFolder.resourceFolder)
}
Expand Down
22 changes: 22 additions & 0 deletions src/main/kotlin/kweb/prelude.kt
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,17 @@ fun ElementCreator<Element>.i(
}
}

open class FooterElement(parent: Element) : Element(parent)

fun ElementCreator<Element>.footer(
attributes: Map<String, JsonPrimitive> = emptyMap(),
new: (ElementCreator<FooterElement>.(FooterElement) -> Unit)? = null
): FooterElement {
return FooterElement(element("footer", attributes)).also {
if (new != null) new(ElementCreator(element = it, insertBefore = null), it)
}
}

open class FormElement(parent: Element) : Element(parent)

fun ElementCreator<Element>.form(
Expand Down Expand Up @@ -256,6 +267,17 @@ fun ElementCreator<Element>.p(
}
}

open class MainElement(parent: Element) : Element(parent)

fun ElementCreator<Element>.main(
attributes: Map<String, JsonPrimitive> = emptyMap(),
new: (ElementCreator<MainElement>.(MainElement) -> Unit)? = null
): MainElement {
return MainElement(element("main", attributes)).also {
if (new != null) new(ElementCreator(element = it, insertBefore = null), it)
}
}

open class NavElement(parent: Element) : Element(parent)

fun ElementCreator<Element>.nav(
Expand Down
15 changes: 11 additions & 4 deletions src/main/kotlin/kweb/routing/UrlToPathSegmentsRF.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ import kweb.state.ReversibleFunction

internal object UrlToPathSegmentsRF : ReversibleFunction<String, List<String>>(label = "UrlToPathSegmentsRF") {
override fun invoke(from: String): List<String> {
return from.substringBefore('?').split('/').drop(1)
// Extract the path segments before the query parameters and URL fragment.
return from.substringBefore('?').substringBefore('#').split('/').drop(1)
}

override fun reverse(original: String, change: List<String>): String {
val queryFragment = original.substringAfter('?', missingDelimiterValue = "")
return '/' + change.joinToString(separator = "/") + (if (queryFragment != "") "?$queryFragment" else "")
// Extract the query parameters and URL fragment from the original URL.
val queryFragment = original.substringAfter('?', missingDelimiterValue = "").substringBefore('#')
val fragment = original.substringAfter('#', missingDelimiterValue = "")

// Reconstruct the URL with the path segments, query parameters, and URL fragment.
return '/' + change.joinToString(separator = "/") +
(if (queryFragment != "") "?$queryFragment" else "") +
(if (fragment != "") "#$fragment" else "")
}
}
}
2 changes: 1 addition & 1 deletion src/main/resources/kweb/kweb_bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function handleInboundMessage(msg) {
for (let i = 0; i < funcCalls.length; i++) {
const funcCall = funcCalls[i];
const debugToken = funcCall["debugToken"];
if (kwebClientId != yourId) {
if (kwebClientId !== yourId) {
console.error(
"Received message from incorrect clientId, was " +
yourId +
Expand Down

0 comments on commit 7660c97

Please sign in to comment.