kweb-core icon indicating copy to clipboard operation
kweb-core copied to clipboard

onImmediate restrictions

Open fjf2002 opened this issue 6 years ago • 16 comments

Hello,

this is my first post on kweb-core. I'm quite excited about the idea of merging server- and client-side programming.

I tested your TODO-example where I saw that the websocket gets quite verbose when filling the input text field, caused by the input.on.keypress event handler code:

input.on.keypress { ke ->
    if (ke.code == "Enter") {
        //etc.

Woudn't that be a case for onImmediate, I first thought. As I read on issue #35 , onImmediate works with pre-recording DOM-changes. As you say in the docs, "the event handler for an onImmediate must limit itself to simple DOM modifications".

  • Do I suppose correctly that even a simple IF statement like above cannot be used in onImmediate because of its nature to just record the effects when executed once on the server side? You should include this in your warning in the docs.

  • If positive, is there no other way of offering a better onImmediate experience, for example

    • writing a Kotlin compiler plugin that is triggered on compiling the "onImmediate" method calls, and compiles the onImmediate callback function parameters to a javascript file that gets included into the website, or
    • let the programmers put the onImmediate callbacks in a separate Kotlin compile unit that gets compiled into JavaScript code? But there must be some magic such that the onImmediate method does not execute the callback but now has to add the right function pointer into the javascript event call. Advantage: The IDE could then better restrict the functions that are allowed to be called. Disadvantage: Separation for the programmer between Kotlin HTML generation and onImmediate callbacks into different compile units.

fjf2002 avatar Oct 16 '19 15:10 fjf2002

Hi, welcome aboard and I appreciate the question :)

You are correct that this could be much more efficient, since the keycode has to be sent to the server every time simply to determine whether the "Enter" key has been pressed.

I think it is probably possible to make onImmediate a lot smarter, perhaps through sophisticated use of coroutines and/or a compiler plugin - however I'm nervous about the complexity this might introduce.

It's not the cleanest approach imaginable, but an easier way to address this specific usecase might be to add a "synthetic" new event, perhaps called input.on.enterKeypress which is only triggered when there is a keypress and the key pressed is 'enter'. This would reduce websocket chatter considerably.

An alternative and perhaps better approach would be to add a blurOnKeypress(vararg keyCodes : String) function to ValueElement, which will cause it to lose focus when one of the specified keys are pressed, and combine this with an on.blur { ... } event handler.

FYI - there is a related inefficiency noted here in the code, ~~I should create an issue for it~~ see #77 .

I'm definitely open to ideas on this.

sanity avatar Oct 16 '19 18:10 sanity

Thank you for your detailed answer. However I should have been more clear that I consider my input.on.keypress case just as an example of a the more general problem that, as far as I understand, onImmediate currently can't evaluate any DOM information and act upon it, e. g.

  • myInput.value = anotherInput.value isn't possible(?)
  • if(myInput.value == 'foo') { ... } isn't possible(?)
  • ...

So, to paraphrase one sentence of yours, I'm rather nervous about what onImmediate currently suggests to be feasible but in reality isn't - if I understand correctly.

fjf2002 avatar Oct 18 '19 07:10 fjf2002

If i may add something here: In my previous project with Kweb, I had the problem of manipulating a Progressbar based on a progress function over time. This had significant challenges, I ended up using Jquery to set the Progress Bar progression every half second, which involved a roundtrip everytime. Additionally, it is very slow, because actually I want to render the bar every 1/30 second to make it seem fluent.

I think these are two problems which have completely different effects, but are in essence the same underlying procedure: How to execute logic on the frontend without involving the server.

There are a few things to consider here: Since Kweb is ment to bring the traditional 3 Tier architecture down to 1 Tier, I think developing with Kweb shouldn´t involve writing Javascript. On the other hand, not every bit of code can be handled by the server, thats just because of the fact that frontend and backend are on two physical locations.

In my opinion there are two solutions to this:

  • Let the user inject Javascript and deliver that to the server
  • Compile immediate events and other frontend-executed Kotlin code to Javascript

Since the first option is not to be desired in my opinion, that would leave us with Kotlin to Javascript compilation. I don´t know if that is feasable or even wanted, since this would tempt the user to write more code on the frontend, which is against a lot that Kweb is trying to solve. On the other hand it would remove a lot of limitation in terms of frontend design Kweb currently has (e.g. Frontend Libraries)

rpanic avatar Oct 18 '19 10:10 rpanic

Pursuing the idea of pre-compiling onImmediate-Lambdas into JavaScript, I've written a small kotlin compiler plugin as a proof-of-concept. On compilation it can

  • remove method calls of methods with name myImmediateEvent
  • and replace them with arbitrary bytecode - in this case: The bytecode for println("Yay, intercepted!...")
  • and log the argument (as text) into a seperate file (see myprintln calls)

The essence of the plugin is:

ExpressionCodegenExtension.registerExtension(project, object : ExpressionCodegenExtension {
    override fun applyFunction(receiver: StackValue, resolvedCall: ResolvedCall<*>, c: ExpressionCodegenExtension.Context): StackValue? {
        val receiverName = resolvedCall.getExplicitReceiverValue()?.type.toString()
        val methodName = resolvedCall.candidateDescriptor.name.toString()

        if(methodName == "myImmediateEvent") {
            val lambdaBodyText = resolvedCall.valueArguments.values.iterator().next().toString() ?: "null"
            val randomLambdaMethodName = "lambda_" + Random.Default.nextLong(1000_0000L, Long.MAX_VALUE).toString(36)

            myprintln("// Yay, lambda body extracted:")
            myprintln("fun $randomLambdaMethodName() $lambdaBodyText")

            return StackValue.functionCall(Type.VOID_TYPE, null) {
                it.apply {
                    // Here we have to generate bytecode on ourselves:
                    getstatic("java/lang/System", "out", "Ljava/io/PrintStream;")
                    visitLdcInsn("Yay, intercepted! Put onkeydown='$randomLambdaMethodName' here")
                    invokevirtual("java/io/PrintStream", "println", "(Ljava/lang/String;)V", false)
                }
            }
        } else {
            return super.applyFunction(receiver, resolvedCall, c);
        }
    }
})

When the following kotlin file gets compiled,

fun myImmediateEvent(callback: () -> Unit) {
    callback()
}

fun main() {
    println("xxxxxxxxxxx")

    myImmediateEvent {
        println("mooo!")
    }
}

... the output is just ...

xxxxxxxxxxx
Yay, intercepted! Put onkeydown='lambda_4sz2pk05wspg' here

... and the myprintln calls generate a file with the following content:

// Yay, lambda body extracted:
fun lambda_4sz2pk05wspg() {
        println("mooo!")
    }

The next steps would be

  • Run the kotlin compiler on the myprintln generated file, with target platform JavaScript
  • include it into the rendered HTML.

Of course I can provide further details on the kotlin compiler plugin if someone's interested.

fjf2002 avatar Oct 18 '19 21:10 fjf2002

This is very interesting, thank you. I don't have much experience with Kotlin compiler plugins, so it might take me some time to wrap my head around it :)

If we could really do this I think it would be the "holy grail", my assumption had been that onImmediate events being treated as simple macros would be sufficient, but based on what you guys are saying it sounds like I was wrong.

If so, that's great news - because if this was an issue for you guys I'm sure it's been an issue for many others.

One quick boring question: Is this likely to add a lot of complexity to people's build.gradle files?

Will review this carefully and I'm sure I'll have more questions. Thank you again :)

sanity avatar Oct 19 '19 01:10 sanity

Oh, just in case you're not aware, you can easily execute JavaScript code from Kweb using WebBrowser.execute, there is also an evaluate function which can return a value.

Obviously it would be much better if the programmer wasn't exposed to JavaScript at all, but it's useful for plugins etc.

sanity avatar Oct 19 '19 01:10 sanity

Oh, @fjf2002, re:

So, to paraphrase one sentence of yours, I'm rather nervous about what onImmediate currently suggests to be feasible but in reality isn't - if I understand correctly.

Do you feel that the warning in the user manual regarding use of onImmediate should be more explicit about its limitations?

sanity avatar Oct 19 '19 01:10 sanity

Sorry for my late answer but here it is:

because if this was an issue for you guys I'm sure it's been an issue for many others.

Well, I haven't used kweb yet, so I can't say that I'm currently having an issue with onImmediate. I was just considering kweb for my next project.

One quick boring question: Is this likely to add a lot of complexity to people's build.gradle files?

I don't have much experience but to achieve the above sketched you should just have to add the compiler plugin to your gradle file, see https://kotlinlang.org/docs/reference/compiler-plugins.html. It should get downloaded automatically from maven if it is published there. Then one more compile step would have to be added to transform the above sketched myprint-Kotlin file to Javascript.

Oh, just in case you're not aware, you can easily execute JavaScript code from Kweb using WebBrowser.execute

Thanks, I missed that.

Do you feel that the warning in the user manual regarding use of onImmediate should be more explicit about its limitations?

Yes.

About Kotlin compiler plugins:

Unfortunately, currently there mainly seem to be Kotlin plugins by JetBrains itself; there is no documentation, and there is no official API.

My resources were

  • https://resources.jetbrains.com/storage/products/kotlinconf2018/slides/5_Writing%20Your%20First%20Kotlin%20Compiler%20Plugin.pdf
  • https://github.com/kevinmost/debuglog
  • https://github.com/JetBrains/kotlin/tree/master/compiler

Further limitations of the suggested approach:

IDE features would not be working properly on the onImmediate lambdas. The IDE would consider the code inside the lambda as part of the Kotlin file it is contained in, whereas we really would cut the lambda out and compile it as a separate function to another platform: Javascript, particularly linking it against different libraries (at least a different Kotlin runtime - the Kotlin Javascript runtime). Not just the IDE has a problem with it - also the programmer would have to understand and accept that. Nevertheless I like the approach.

fjf2002 avatar Oct 22 '19 19:10 fjf2002

BUMP Any thoughts/opinions on whether to go in this direction?

fjf2002 avatar Oct 30 '19 12:10 fjf2002

Sorry about the delay.

I see the appeal of the approach ,but I'm concerned about its complexity in practice and fear there might be pitfalls here that we're unaware of.

For example, what happens if code within an onImmediate block attempts to access server-side state?

Writing code to files in order to compile also sounds like it could add significant complexity to our build process, while not necessarily solving problems like the one I mentioned above.

sanity avatar Oct 30 '19 20:10 sanity

@fjf2002 I've recently learned more about macros in Rust and if Kotlin's compiler plugins have similar power - then I think there could be a way to do this.

The plugin would look for callback code blocks like:

button.on.click {
   warningSpan.text("Clicked!")
   println("Disallowed")
}

Starting at the first line, it would check to verify that only "immediate-safe" function calls occur, these would be things like making DOM changes - but ANY other method call would be disallowed (a whitelist, rather than a blacklist).

Then, these lines can (hopefully!) safely be moved into a preceding button.onImmediate.click using some compiler plugin code rewriting wizardry, perhaps something like:

button.onImmediate.click {
   warningSpan.text("Clicked!")
}
button.on.click {
   println("Disallowed")
}

sanity avatar Mar 03 '20 09:03 sanity

Hello @sanity, nice to hear from you. Meanwhile in the project that I work in we decided not to use Kotlin, i.e. kwebio is not an option any more. Nevertheless I can provide my opinion and the code I did so far for some "compiler plugin" as described above.

it would check to verify that only "immediate-safe" function calls occur

I don't know how you can do this in Kotlin, but is this even theoretically possible? How would that work in Rust? You would have to disallow every single function call, including Object instantiation, of functions/classes that have no implementation for the Kotlin/JS platform(?)

Then, these lines can (hopefully!) safely be moved into a preceding button.onImmediate.click

What sense would that make if immediate-safe and non-immediate-safe statements are interleaved, perhaps depend on the results (i.e. variable assignments) of each other, respectively?

So the latter idea seems to be much more complicated than the former. My conclusion would be that if a lambda contains non-immediate function calls, it should be considered "non-immediateable" as a whole - the compiler could decide to give a warning and compile it as a usual non-immediate.

fjf2002 avatar Mar 03 '20 14:03 fjf2002

You would have to disallow every single function call, including Object instantiation, of functions/classes that have no implementation for the Kotlin/JS platform(?)

Yeah, this could get very messy at the edges.

What sense would that make if immediate-safe and non-immediate-safe statements are interleaved, perhaps depend on the results (i.e. variable assignments) of each other, respectively?

My thought was that if the callback consisted of some DOM modifications at the beginning followed by some other logic, then the DOM modifications could be automatically moved into an onImmediate by the plugin.

sanity avatar Mar 04 '20 18:03 sanity

What if you used synthetic control flows via kotlin dsl? Would you be able to generate the correct js with something like:

kIf (...) { ... }

shumy avatar Jul 25 '20 01:07 shumy

It's possible, but I'm concerned that it could get complicated quickly.

sanity avatar Jul 25 '20 21:07 sanity

It's possible, but I'm concerned that it could get complicated quickly.

Yes, probably. Besides that I like the @fjf2002 solution. But I believe you should be explicit about the developer intent, maintaining the use of onImmediate. In case immediate is not possible, it should fallback to non-immediate and throw a warning, or even an error (depending on a compiler plugin flag).

Now, a bit of topic here (but maybe you can catch some ideas): I found you library because I was thinking about doing something similar. But one needs to search before reinventing the wheel. However my idea was more opinionated about the architecture. The way I though about solving the problem was like:

@Component
class SomeViewComponent {
  @Bind val name = KVar("John")
  @Bind var number = 10

  fun render() {
    // DOM changes
    button.onClick(this::btnClick)
  }
  
  @Client(forward="serverBtnClick")
  fun clientBtnClick() {
    // Do something client side
    // The client function is normally scoped for client variables only, but it can interact with server side binds
    name.value = "Alex"
    number = 20
  }

  @Server()
  fun serverBtnClick() {
  }
}

// registering the component on a server instance
server.register(SomeViewComponent::class)

And then use the register function and reflection to parse clientBtnClick into javascript code, instead of using a compiler plugin.

However, I didn't check if such approach would work. Just an idea. The main motivation about putting effort on such project is because Vaddin is getting away from the main philosophy, by introducing Polymer. Solving this problem is indeed the Holy Grail for these kind of projects.

shumy-tools avatar Jul 27 '20 09:07 shumy-tools

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.

github-actions[bot] avatar Oct 15 '22 03:10 github-actions[bot]

Closing for now, can reopen if others have this issue.

sanity avatar Oct 16 '22 17:10 sanity