Skip to content

Commit 959ec96

Browse files
committed
wip: react ssr support
Signed-off-by: Dario Valdespino <dvaldespino00@gmail.com>
1 parent 146ffa1 commit 959ec96

3 files changed

Lines changed: 114 additions & 0 deletions

File tree

packages/graalvm-py/src/main/resources/META-INF/elide/embedded/runtime/python/init.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def __init__interop():
3333
FLASK_URL_FOR = "url_for"
3434
FLASK_MAKE_RESPONSE = "make_response"
3535
FLASK_REDIRECT = "redirect"
36+
FLASK_REACT = "react"
3637

3738
MODULE_NAME = "elide"
3839

@@ -60,6 +61,15 @@ def polyglot_decorator(name = None):
6061
def flask_binding():
6162
return polyglot.import_value(_private_symbol_name(FLASK_ENTRY))
6263

64+
def react(source_path: str):
65+
flask = flask_binding()
66+
entrypoint = flask.react_template(source_path)
67+
68+
def render(**params):
69+
return entrypoint(params)
70+
71+
return render
72+
6373
def redirect(target):
6474
response = flask_binding().make_response()
6575
response.status = 302
@@ -154,6 +164,8 @@ def __getattr__(self, name):
154164
return flask_binding().make_response
155165
if name == FLASK_REDIRECT:
156166
return redirect
167+
if name == FLASK_REACT:
168+
return react
157169
raise AttributeError(f"module '{MODULE_NAME}' has no attribute '{name}'")
158170

159171
def __dir__(self):

packages/graalvm/src/main/kotlin/elide/runtime/intrinsics/server/http/v2/flask/FlaskHttpIntrinsic.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ private const val BIND_METHOD = "bind"
181181
@Deprecated("Use symbolicName instead")
182182
override fun displayName(): String = FlaskAPI.FLASK_INTRINSIC
183183
override fun getMember(key: String?): Any? = when (key) {
184+
"react_template" -> ProxyExecutable { args ->
185+
val relativePath = args.firstOrNull()?.takeIf { it.isString }?.asString()
186+
?: error("Expected a string argument for `react`")
187+
188+
val scriptPath = applicationRoot?.resolve(relativePath)
189+
?: error("No location info available for the current application")
190+
191+
FlaskReactTemplate(scriptPath)
192+
}
193+
184194
"request" -> requestAccessor
185195
"abort" -> ProxyExecutable { args ->
186196
val code = args.firstOrNull()?.takeIf { it.isNumber }?.asInt()
@@ -379,6 +389,17 @@ private const val BIND_METHOD = "bind"
379389
httpContext.responseBody.source(DefaultLastHttpContent(content))
380390
}
381391

392+
returnValue.isHostObject -> {
393+
// it could be a content producer
394+
val producer = runCatching { returnValue.asHostObject<HttpContentSink.Producer>() }
395+
.getOrNull()
396+
?: error("Invalid Flask response object provided: $returnValue")
397+
398+
if (overrideStatus) httpContext.response.status = HttpResponseStatus.OK
399+
httpContext.response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, "chunked")
400+
httpContext.responseBody.source(producer)
401+
}
402+
382403
returnValue.isProxyObject -> {
383404
// assume it is a Flask response object
384405
val response = runCatching { returnValue.asProxyObject<FlaskResponseObject>() }
@@ -412,6 +433,7 @@ private const val BIND_METHOD = "bind"
412433
"abort",
413434
"url_for",
414435
"make_response",
436+
"react_template",
415437
)
416438
}
417439
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright (c) 2024-2025 Elide Technologies, Inc.
3+
*
4+
* Licensed under the MIT license (the "License"); you may not use this file except in compliance
5+
* with the License. You may obtain a copy of the License at
6+
*
7+
* https://opensource.org/license/mit/
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11+
* License for the specific language governing permissions and limitations under the License.
12+
*/
13+
14+
package elide.runtime.intrinsics.server.http.v2.flask
15+
16+
import io.netty.buffer.Unpooled
17+
import io.netty.handler.codec.http.DefaultLastHttpContent
18+
import org.graalvm.polyglot.Context
19+
import org.graalvm.polyglot.Source
20+
import org.graalvm.polyglot.Value
21+
import org.graalvm.polyglot.proxy.ProxyExecutable
22+
import java.nio.file.Path
23+
import java.util.concurrent.atomic.AtomicReference
24+
import kotlin.concurrent.getOrSet
25+
import elide.runtime.Logging
26+
import elide.runtime.intrinsics.js.JsPromise
27+
import elide.runtime.intrinsics.server.http.v2.HttpContentSink
28+
29+
internal class FlaskReactTemplate(private val source: Path) : ProxyExecutable {
30+
private val sourceScript by lazy {
31+
Source.newBuilder("ts", source.toFile())
32+
.build()
33+
}
34+
35+
private val template = ThreadLocal<Value>()
36+
37+
override fun execute(vararg arguments: Value?): Any {
38+
val parsedTemplate = template.getOrSet {
39+
Context.getCurrent().eval(sourceScript).getMember("default")
40+
}
41+
42+
val result = parsedTemplate.execute(arguments.firstOrNull())
43+
val promise = JsPromise.wrapOrNull(
44+
value = result,
45+
unwrapFulfilled = { it!!.asString() },
46+
unwrapRejected = { it },
47+
)
48+
49+
if (promise == null) {
50+
if (!result.isString) error("React template did not render to a string and did not return a promise")
51+
else return result.asString()
52+
}
53+
54+
return object : HttpContentSink.Producer {
55+
private val pulled = AtomicReference<HttpContentSink.Handle>()
56+
57+
override fun pull(handle: HttpContentSink.Handle) {
58+
if (!promise.isDone) pulled.set(handle)
59+
promise.then(
60+
onFulfilled = { rendered ->
61+
pulled.get()?.push(DefaultLastHttpContent(Unpooled.copiedBuffer(rendered, Charsets.UTF_8)))
62+
handle.release(close = true)
63+
},
64+
onCatch = {
65+
logging.error("Failed to render React template: $it")
66+
handle.release(close = true)
67+
},
68+
)
69+
}
70+
71+
override fun released() {
72+
pulled.set(null)
73+
}
74+
}
75+
}
76+
77+
private companion object {
78+
private val logging by lazy { Logging.of(FlaskReactTemplate::class) }
79+
}
80+
}

0 commit comments

Comments
 (0)