// Sol Routes - Mars registration and request handling
//
// Registers compiled SolRoutes to the Hono/Mars app and handles requests.
// ============================================================================
// Route Registration to Hono
// ============================================================================
///|
fn ensure_mars_to_handler_compat() -> Unit {
ffi_ensure_mars_to_handler_reschedule_symbol()
}
///|
/// Register Sol routes to Hono app
pub fn register_sol_routes(
app : App,
routes : Array[SolRoutes],
config? : RouterConfig,
) -> Unit {
ensure_mars_to_handler_compat()
let compiled = compile_sol_routes(routes)
let cfg = config.unwrap_or(RouterConfig::default())
register_compiled_sol_routes(app, compiled, cfg)
}
///|
fn is_isr_with_cache(config : RouterConfig, route_revalidate : Int?) -> Bool {
match (route_revalidate, config.isr_cache) {
(Some(_), Some(_)) => true
_ => false
}
}
///|
fn should_use_streaming(
config : RouterConfig,
is_fragment : Bool,
route_revalidate : Int?,
) -> Bool {
config.enable_streaming &&
!(is_fragment) &&
!(is_isr_with_cache(config, route_revalidate))
}
///|
fn cache_strategy_to_header_value(strategy : CacheStrategy) -> String {
match strategy {
CacheStrategy::AlwaysFetch => "always-fetch"
CacheStrategy::StaleWhileRevalidate => "stale-while-revalidate"
}
}
///|
/// Apply CDN cache headers based on CDNCacheStrategy configuration.
/// Sets Cache-Control and X-Sol-CDN-Cache headers for ISR pages.
fn apply_cdn_cache_headers(
c : @mars.Context,
strategy : @isr.CDNCacheStrategy?,
revalidate : Int,
) -> Unit {
match strategy {
Some(s) => {
c.set_header("Cache-Control", s.cache_control(revalidate))
c.set_header("X-Sol-CDN-Cache", s.header_value())
}
None => ()
}
}
///|
fn apply_layouts(
content : @server_dom.ServerNode,
props : PageProps,
layouts : Array[
(PageProps, @server_dom.ServerNode) -> @server_dom.ServerNode raise Error,
],
is_fragment : Bool,
) -> @server_dom.ServerNode raise Error {
// Apply layouts from innermost to outermost
// For fragments: skip root layout (index 0), apply nested layouts
// For full pages: apply all layouts
let mut node = content
let end_idx = if is_fragment { 1 } else { 0 }
let mut i = layouts.length() - 1
while i >= end_idx {
let layout_fn = layouts[i]
node = layout_fn(props, node)
i -= 1
}
node
}
///|
/// Render page and return HTML (helper for ISR)
async fn render_page_to_html(
c : Ctx,
props : PageProps,
handler : async (PageProps) -> @server_dom.ServerNode,
layouts : Array[
(PageProps, @server_dom.ServerNode) -> @server_dom.ServerNode raise Error,
],
is_fragment : Bool,
title : String,
page_head : String,
root_template : String,
cache_strategy : CacheStrategy,
) -> String raise Error {
// Call handler and apply layouts
let content : @server_dom.ServerNode = handler(props)
if c.is_response_sent() {
fail("ISR render attempted to send a direct response")
}
let node = apply_layouts(content, props, layouts, is_fragment)
if c.is_response_sent() {
fail("ISR render attempted to mutate the response")
}
if is_fragment {
render_server_fragment(node, title, cache_strategy)
} else {
render_server_page_html(node, title, page_head, root_template)
}
}
///|
async fn send_non_isr_page_response(
c : Ctx,
props : PageProps,
handler : async (PageProps) -> @server_dom.ServerNode,
layouts : Array[
(PageProps, @server_dom.ServerNode) -> @server_dom.ServerNode raise Error,
],
is_fragment : Bool,
page_title : String,
page_head : String,
root_template : String,
cache_strategy : CacheStrategy,
route_revalidate : Int?,
config : RouterConfig,
) -> Unit {
let content : @server_dom.ServerNode = handler(props) catch {
e => {
log_server_error("Page handler error", e)
return c.text("Internal Server Error", status=500)
}
}
if c.is_response_sent() {
return
}
let node = apply_layouts(content, props, layouts, is_fragment) catch {
e => {
log_server_error("Layout error", e)
return c.text("Internal Server Error", status=500)
}
}
if is_fragment {
let fragment_html = render_server_fragment(node, page_title, cache_strategy)
send_fragment_response(c, fragment_html)
} else if should_use_streaming(config, is_fragment, route_revalidate) {
send_streaming_server_page(
c,
node,
page_title,
page_head,
config.loader_url,
root_template,
)
} else {
let html = render_server_page_html(
node, page_title, page_head, root_template,
)
c.html(html)
}
}
///|
async fn handle_compiled_api_route(
c : Ctx,
route : CompiledSolRoute,
middleware_chain : @middleware.Middleware,
) -> Unit {
// Run middleware chain first
let halted = @middleware.run_or_500(middleware_chain, c)
if halted {
return
}
let params = extract_route_params(
c,
is_fragment=false,
param_names=route.param_names,
)
let props = PageProps::from_ctx_with_params(c, params)
match route.api_handler {
Some(ApiHandler(handler)) => {
let json : Json = handler(props) catch {
e => {
log_server_error("API handler error", e)
@mars_response.send_internal_error(c)
return
}
}
if c.is_response_sent() {
return
}
@mars_response.send_json(c, json)
}
None => @mars_response.send_json_error(c, "No handler", status=404)
}
}
///|
fn is_api_route_kind(kind : SolRouteKind) -> Bool {
match kind {
SolRouteKind::GetApi => true
SolRouteKind::PostApi => true
SolRouteKind::PutApi => true
SolRouteKind::DeleteApi => true
SolRouteKind::PatchApi => true
SolRouteKind::Page => false
}
}
///|
fn register_compiled_api_route(
app : App,
kind : SolRouteKind,
hono_pattern : String,
route : CompiledSolRoute,
middleware_chain : @middleware.Middleware,
) -> Unit {
let handler = async fn(c) {
handle_compiled_api_route(c, route, middleware_chain)
}
match kind {
SolRouteKind::GetApi => app.get(hono_pattern, handler)
SolRouteKind::PostApi => app.post(hono_pattern, handler)
SolRouteKind::PutApi => app.put(hono_pattern, handler)
SolRouteKind::DeleteApi => app.delete(hono_pattern, handler)
SolRouteKind::PatchApi => app.patch(hono_pattern, handler)
SolRouteKind::Page => ()
}
}
///|
/// Handle an ISR-enabled page request. Called when revalidate and isr_cache
/// are both present and the request is not a fragment.
async fn handle_isr_page_request(
c : Ctx,
props : PageProps,
handler : async (PageProps) -> @server_dom.ServerNode,
layouts : Array[
(PageProps, @server_dom.ServerNode) -> @server_dom.ServerNode raise Error,
],
page_title : String,
page_head : String,
root_template : String,
cache_strategy : CacheStrategy,
route_revalidate : Int?,
revalidate : Int,
cache : @isr.MemoryCache,
config : RouterConfig,
) -> Unit {
if !(props.params.query.is_empty()) {
c.set_header("Cache-Control", "private, no-store")
c.set_header("X-Sol-ISR-Status", "BYPASS")
return send_non_isr_page_response(
c,
props,
handler,
layouts,
false,
page_title,
page_head,
root_template,
cache_strategy,
route_revalidate,
config,
)
}
// Apply CDN cache headers if strategy is configured
apply_cdn_cache_headers(c, config.cache_strategy, revalidate)
// ISR enabled - check cache
let cache_key = @isr.cache_key(props.params.path, props.params.query)
let entry = @isr.ISRCache::get(cache, cache_key)
let now = @isr.now_ms()
let status = @isr.check_status(entry, now)
// Set X-Sol-ISR-Status header for diagnostics
let isr_status_str = match status {
@isr.Fresh => "HIT"
@isr.Stale => "STALE"
@isr.Miss => "MISS"
}
c.set_header("X-Sol-ISR-Status", isr_status_str)
// ISR render props: mark as ISR to warn against ctx header access
let isr_props = { ..props, is_isr_render: true }
match status {
@isr.Fresh =>
// Return cached HTML directly
match entry {
Some(e) => c.html(e.html)
None => c.text("Cache error", status=500)
}
@isr.Stale =>
// Return cached HTML and schedule background revalidation
match entry {
Some(e) => {
// Deduplicate in-flight revalidations for same cache key
if !(isr_in_flight.val.contains(cache_key)) {
isr_in_flight.val.add(cache_key)
@isr.ISRCache::schedule_revalidation(cache, async fn() {
let html = render_page_to_html(
c,
isr_props,
handler,
layouts,
false,
page_title,
page_head,
root_template,
cache_strategy,
) catch {
_ => {
isr_in_flight.val.remove(cache_key)
return
}
}
let new_entry = @isr.CacheEntry::new(
html,
@isr.now_ms(),
revalidate,
)
@isr.ISRCache::put(cache, cache_key, new_entry)
isr_in_flight.val.remove(cache_key)
})
}
c.html(e.html)
}
None => c.text("Cache error", status=500)
}
@isr.Miss => {
// Render, cache, and return
let html = render_page_to_html(
c,
isr_props,
handler,
layouts,
false,
page_title,
page_head,
root_template,
cache_strategy,
) catch {
e => {
log_server_error("ISR revalidation error", e)
return c.text("Internal Server Error", status=500)
}
}
let new_entry = @isr.CacheEntry::new(html, now, revalidate)
@isr.ISRCache::put(cache, cache_key, new_entry)
c.html(html)
}
}
}
///|
fn register_compiled_sol_routes(
app : App,
routes : Array[CompiledSolRoute],
config : RouterConfig,
) -> Unit {
// Register server island defer endpoint if registry is configured
match config.server_islands {
Some(registry) =>
if !(registry.is_empty()) {
register_defer_endpoint(app, registry)
}
None => ()
}
for route in routes {
// Convert catch-all pattern to Hono wildcard pattern
let hono_pattern = convert_to_hono_pattern(route.pattern, route.catch_all)
let middleware_chain = @middleware.pipeline(route.middlewares)
match route.kind {
SolRouteKind::Page =>
app.get(hono_pattern, async fn(c) {
// Run middleware chain first
let halted = @middleware.run_or_500(middleware_chain, c)
if halted {
return
}
let is_fragment = has_fragment_header(c)
let params = extract_route_params(
c,
is_fragment~,
param_names=route.param_names,
)
// Add catch-all parameter if present
let props = PageProps::from_ctx_with_params(c, params)
let page_title = format_title(
route.title,
title_prefix=config.title_prefix,
default_title=config.default_title,
)
let page_head = config.default_head + route.rendered_meta
c.set_header(
"X-Sol-Cache-Strategy",
cache_strategy_to_header_value(route.cache_strategy),
)
match route.page_handler {
Some(PageHandler(handler)) =>
// Check if ISR is enabled for this route (only for full page requests)
match (route.revalidate, config.isr_cache, is_fragment) {
(Some(revalidate), Some(cache), false) =>
handle_isr_page_request(
c,
props,
handler,
route.layouts,
page_title,
page_head,
config.root_template,
route.cache_strategy,
route.revalidate,
revalidate,
cache,
config,
)
_ =>
send_non_isr_page_response(
c,
props,
handler,
route.layouts,
is_fragment,
page_title,
page_head,
config.root_template,
route.cache_strategy,
route.revalidate,
config,
)
}
None => c.text("No handler for route", status=404)
}
})
kind =>
if is_api_route_kind(kind) {
register_compiled_api_route(
app, kind, hono_pattern, route, middleware_chain,
)
}
}
}
}
///|
/// Render ServerNode to fragment HTML (for CSR navigation)
async fn render_server_fragment(
node : @server_dom.ServerNode,
title : String,
cache_strategy : CacheStrategy,
) -> String {
// Resolve the ServerNode to get the actual Node
let resolved_node = node.resolve()
let body_html = @render.render_to_string(resolved_node).html
// Wrap in templates like fragment.mbt does
let buf = StringBuilder::new()
buf.write_string("")
buf.write_string(body_html)
buf.write_string("")
buf.write_string("")
buf.write_string(@page_shell.escape_html_text(title))
buf.write_string("")
buf.write_string("")
buf.write_string(cache_strategy_to_header_value(cache_strategy))
buf.write_string("")
buf.to_string()
}
///|
/// Render ServerNode to full page HTML
async fn render_server_page_html(
node : @server_dom.ServerNode,
title : String,
head : String,
template : String,
) -> String {
// Resolve the ServerNode to get the actual Node
let resolved_node = node.resolve()
let body_html = @render.render_to_string(resolved_node).html
// Add HMR script in dev mode
let full_head = @hot_reload.with_dev_head_script(head)
@page_shell.render_template(template, title, full_head, "", body_html)
}