// JS-specific FFI for sol_routes
///|
extern "js" fn ffi_console_error(msg : String) -> Unit =
#| (msg) => console.error(msg)
///|
extern "js" fn ffi_ensure_mars_to_handler_reschedule_symbol() -> Unit =
#| () => {
#| if (typeof globalThis.moonbitlang$async$internal$event_loop$$reschedule !== "function") {
#| globalThis.moonbitlang$async$internal$event_loop$$reschedule = () => {};
#| }
#| }
///|
extern "js" fn ffi_create_streaming_response(
render_with_async : async ((String) -> Unit) -> Unit,
) -> @js.Any =
#| (renderWithAsync) => new ReadableStream({
#| async start(controller) {
#| const encoder = new TextEncoder();
#| const write = (chunk) => controller.enqueue(encoder.encode(chunk));
#| try {
#| await renderWithAsync(write);
#| controller.close();
#| } catch (e) {
#| console.error("[sol] Streaming SSR error:", e);
#| try { controller.error(e); } catch (_) {}
#| }
#| }
#| })
///|
extern "js" fn ffi_set_streaming_response(ctx : Ctx, stream : @js.Any) -> Unit =
#| (ctx, stream) => {
#| const headers = { 'Content-Type': 'text/html; charset=utf-8' };
#| const map = ctx.response_headers;
#| let curr = map ? map.head : undefined;
#| while (curr !== undefined && curr !== -1) {
#| headers[curr.key] = curr.value;
#| curr = curr.next;
#| }
#| const response = new Response(stream, { headers });
#| ctx.platform.response_resolve(response);
#| ctx.response_sent = true;
#| }
///|
extern "js" fn ffi_send_web_response(
ctx : Ctx,
response : @web_http.Response,
) -> Unit =
#| (ctx, response) => {
#| let outgoing = response;
#| const map = ctx.response_headers;
#| let curr = map ? map.head : undefined;
#| if (curr !== undefined && curr !== -1) {
#| const headers = new Headers(response.headers);
#| while (curr !== undefined && curr !== -1) {
#| if (!headers.has(curr.key)) {
#| headers.set(curr.key, curr.value);
#| }
#| curr = curr.next;
#| }
#| outgoing = new Response(response.body, {
#| status: response.status,
#| statusText: response.statusText,
#| headers,
#| });
#| }
#| ctx.platform.response_resolve(outgoing);
#| ctx.response_sent = true;
#| }
///|
/// Send streaming SSR page via JS ReadableStream.
/// The header/footer/rendering logic is portable; only the transport is JS-specific.
async fn send_streaming_server_page(
c : Ctx,
node : @server_dom.ServerNode,
title : String,
head : String,
loader_url : String,
root_template : String,
) -> Unit {
let full_head = @hot_reload.with_dev_head_script(head)
let (header_html, footer_html) = match
@page_shell.render_streaming_template_parts(
root_template, title, full_head, "", loader_url,
) {
Some(parts) => parts
None =>
(
@page_shell.render_streaming_header(title, full_head, loader_url),
@page_shell.render_streaming_footer(),
)
}
// JS: use ReadableStream for chunked transfer encoding
// Header is flushed immediately (fast TTFB), then node is resolved
// inside the stream so async data fetching happens during streaming.
let stream = ffi_create_streaming_response(async fn(write) {
write(header_html)
let resolved_node = node.resolve()
@portable_stream.render_to_stream_with_async(
resolved_node,
@portable_stream.StreamWriter::from_callback(write),
)
write(footer_html)
})
ffi_set_streaming_response(c, stream)
}