-module(libero@codegen_server). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/libero/codegen_server.gleam"). -export([write_websocket/2, write_atoms/2, write_if_missing/2, write_main/6]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Server-side output generators.\n" "\n" " Emits `websocket.gleam`, the atoms registration `.erl` file, and the\n" " `.gleam` entry point. These all land in the server package and\n" " only compile on the Erlang target.\n" ). -file("src/libero/codegen_server.gleam", 20). ?DOC( " Generate the server websocket handler at `server_generated/websocket.gleam`.\n" " Also writes the Erlang FFI for decoding push messages.\n" ). -spec write_websocket(binary(), binary()) -> {ok, nil} | {error, libero@gen_error:gen_error()}. write_websocket(Server_generated, Context_module) -> Gleam_output = <>, Module_path = libero@codegen:last_split(Server_generated, <<"src/"/utf8>>), Content = <<<<<<<<"//// Code generated by libero. DO NOT EDIT. //// //// WebSocket handler for mist. Handles dispatch, push frame //// forwarding, and topic cleanup on disconnect. import gleam/dynamic/decode import gleam/erlang/atom import gleam/erlang/process import gleam/http/request.{type Request} import gleam/list import gleam/option.{type Option, Some} import gleam/result import gleam/string import libero/push import libero/ws_logger.{type Logger} import mist.{type Connection} import "/utf8, Module_path/binary>>/binary, "/dispatch import "/utf8>>/binary, Context_module/binary>>/binary, ".{type HandlerContext} pub type ConnState { ConnState(handler_ctx: HandlerContext, topics: List(String), logger: Logger) } type PushMsg { PushFrame(BitArray) Ignored } /// Wire up a WebSocket upgrade with dispatch, push, and topic management. /// The `logger` argument receives connect/disconnect, panic, and send /// failure messages. Use `ws_logger.default_logger()` for stdout output /// or pass your own structured logger. pub fn upgrade( request req: Request(Connection), handler_ctx handler_ctx: HandlerContext, topics topics: List(String), logger logger: Logger, ) { mist.websocket( request: req, handler: handler, on_init: on_init(handler_ctx, topics, logger), on_close: fn(conn_state: ConnState) { list.each(conn_state.topics, fn(t) { push.leave(topic: t) }) logger.debug(\"WebSocket: disconnected\") }, ) } fn on_init(handler_ctx: HandlerContext, topics: List(String), logger: Logger) { fn(_conn: mist.WebsocketConnection) -> #(ConnState, Option(process.Selector(PushMsg))) { logger.debug(\"WebSocket: connected\") list.each(topics, fn(t) { push.join(topic: t) }) let selector = process.new_selector() |> process.select_record( tag: atom.create(\"libero_push\"), fields: 1, mapping: fn(record) { { use frame <- decode.field(1, decode.bit_array) decode.success(PushFrame(frame)) } |> decode.run(record, _) |> result.unwrap(Ignored) }, ) #(ConnState(handler_ctx:, topics:, logger:), Some(selector)) } } fn handler( conn_state: ConnState, message: mist.WebsocketMessage(PushMsg), conn: mist.WebsocketConnection, ) { case message { mist.Binary(data) -> { let #(response_bytes, maybe_panic, new_handler_ctx) = dispatch.handle(handler_ctx: conn_state.handler_ctx, data:) case maybe_panic { Some(info) -> conn_state.logger.error( \"RPC panic: \" <> info.fn_name <> \" (trace \" <> info.trace_id <> \"): \" <> info.reason, ) _ -> Nil } case mist.send_binary_frame(conn, response_bytes) { Ok(_) -> Nil Error(reason) -> conn_state.logger.warning( \"Failed to send WebSocket frame: \" <> string.inspect(reason), ) } mist.continue(ConnState(..conn_state, handler_ctx: new_handler_ctx)) } mist.Custom(PushFrame(frame)) -> { case mist.send_binary_frame(conn, frame) { Ok(_) -> Nil Error(reason) -> conn_state.logger.warning( \"Failed to send WebSocket push frame: \" <> string.inspect(reason), ) } mist.continue(conn_state) } mist.Custom(Ignored) -> mist.continue(conn_state) mist.Closed | mist.Shutdown -> mist.stop() mist.Text(_) -> mist.continue(conn_state) } } "/utf8>>, libero@codegen:ensure_parent_dir(Gleam_output), libero@codegen:write_file(Gleam_output, Content). -file("src/libero/codegen_server.gleam", 154). ?DOC( " Generate an Erlang FFI file that pre-registers all constructor atoms\n" " discovered by the type graph walker, plus framework atoms used by\n" " libero's wire protocol. Calling `ensure/0` from this module creates\n" " the atoms in the BEAM atom table so that `binary_to_term([safe])`\n" " can decode client ETF payloads without rejecting unknown atoms.\n" ). -spec write_atoms(libero@config:config(), list(libero@walker:discovered_type())) -> {ok, nil} | {error, libero@gen_error:gen_error()}. write_atoms(Config, Discovered) -> Framework_atoms = [<<"ok"/utf8>>, <<"error"/utf8>>, <<"some"/utf8>>, <<"none"/utf8>>, <<"nil"/utf8>>, <<"true"/utf8>>, <<"false"/utf8>>, <<"malformed_request"/utf8>>, <<"unknown_function"/utf8>>, <<"internal_error"/utf8>>, <<"decode_error"/utf8>>], Discovered_atoms = begin _pipe = gleam@list:flat_map( Discovered, fun(T) -> erlang:element(5, T) end ), gleam@list:map(_pipe, fun(V) -> erlang:element(4, V) end) end, All_atoms = begin _pipe@1 = lists:append(Framework_atoms, Discovered_atoms), _pipe@2 = gleam@list:unique(_pipe@1), gleam@list:sort(_pipe@2, fun gleam@string:compare/2) end, Atom_list = begin _pipe@3 = gleam@list:map( All_atoms, fun(Atom) -> <<<<" <<\""/utf8, Atom/binary>>/binary, "\">>"/utf8>> end ), gleam@string:join(_pipe@3, <<",\n"/utf8>>) end, Content = <<<<<<<<"%% Code generated by libero. DO NOT EDIT. %% %% Pre-registers all constructor atoms that may appear in client ETF %% payloads, so binary_to_term([safe]) can decode them without %% rejecting unknown atoms. %% %% ensure/0 uses persistent_term as a one-shot guard so the %% binary_to_atom calls only run once per VM lifetime. %% %% lists:foreach + fun is used instead of bare binary_to_atom calls %% because the Erlang compiler optimizes away pure BIF calls whose %% results are discarded. -module("/utf8, (erlang:element(4, Config))/binary>>/binary, "). -export([ensure/0]). ensure() -> case persistent_term:get({?MODULE, done}, false) of true -> nil; false -> do_ensure() end. do_ensure() -> lists:foreach(fun(B) -> binary_to_atom(B) end, [ "/utf8>>/binary, Atom_list/binary>>/binary, " ]), persistent_term:put({?MODULE, done}, true), nil. "/utf8>>, Output = erlang:element(3, Config), libero@codegen:ensure_parent_dir(Output), libero@codegen:write_file(Output, Content). -file("src/libero/codegen_server.gleam", 428). ?DOC( " Write content to a file, skipping if the file already exists.\n" " Used for scaffolded files that users may customize after generation.\n" ). -spec write_if_missing(binary(), binary()) -> {ok, nil} | {error, libero@gen_error:gen_error()}. write_if_missing(Path, Content) -> case begin _pipe = simplifile_erl:is_file(Path), gleam@result:unwrap(_pipe, false) end of true -> gleam_stdlib:println( <<<<" kept "/utf8, Path/binary>>/binary, " (exists, not overwriting)"/utf8>> ), {ok, nil}; false -> libero@codegen:write_file(Path, Content) end. -file("src/libero/codegen_server.gleam", 211). ?DOC( " Generate the server entry point at\n" " `/src/.gleam`.\n" ). -spec write_main( binary(), integer(), binary(), binary(), list(binary()), binary() ) -> {ok, nil} | {error, libero@gen_error:gen_error()}. write_main( App_name, Port, Server_generated, Context_module, Js_client_names, Project_path ) -> Output = <<<<<>/binary, (gleam@string:replace(App_name, <<"-"/utf8>>, <<"_"/utf8>>))/binary>>/binary, ".gleam"/utf8>>, Dispatch_module = libero@codegen:last_split( Server_generated, <<"src/"/utf8>> ), Ws_module = <>, {Js_routes, Index_route} = case Js_client_names of [] -> {<<""/utf8>>, <<" _, _ -> response.new(404) |> response.set_body(mist.Bytes(bytes_tree.from_string(\"Not found\")))"/utf8>>}; [First_js | _] -> Routes = begin _pipe = gleam@list:map( Js_client_names, fun(Name) -> <<<<<<<<" _, [\""/utf8, Name/binary>>/binary, "\", ..path] -> serve_file( \"clients/"/utf8>>/binary, Name/binary>>/binary, "/build/dev/javascript/\" <> string.join(list.filter(path, fn(s) { s != \"..\" && s != \"\" }), \"/\"), )"/utf8>> end ), _pipe@1 = gleam@string:join(_pipe, <<"\n"/utf8>>), (fun(R) -> <<"\n"/utf8, R/binary>> end)(_pipe@1) end, Index = <<<<<<<<" _, _ -> serve_html(\"
\")"/utf8>>, {Routes, Index} end, Context_qualifier = libero@codegen:last_split(Context_module, <<"/"/utf8>>), Content = <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<"//// Generated by libero as a starting point. Customize for your application. //// //// Server-only: this module starts a mist HTTP server and depends on //// Erlang-target generated dispatch. Customize freely; it won't compile to JS. import gleam/bytes_tree import gleam/erlang/process import gleam/http import gleam/http/request.{type Request} import gleam/http/response import gleam/io import gleam/list import gleam/option.{None, Some} import gleam/string import libero/push import libero/ws_logger import mist.{type Connection} import "/utf8, Dispatch_module/binary>>/binary, "/dispatch import "/utf8>>/binary, Ws_module/binary>>/binary, " as ws import "/utf8>>/binary, Context_module/binary>>/binary, " // Server entry point. Initializes the push system (for server-to-client // broadcasts), registers atoms for safe ETF decoding, creates the shared // HandlerContext, and starts a mist HTTP server with WebSocket and RPC routes. pub fn main() { let _ = push.init() let _ = dispatch.ensure_atoms() let handler_ctx = "/utf8>>/binary, Context_qualifier/binary>>/binary, ".new() let logger = ws_logger.default_logger() let started = fn(req: Request(Connection)) { case req.method, request.path_segments(req) { _, [\"ws\"] -> ws.upgrade( request: req, handler_ctx:, topics: [], logger:, ) http.Post, [\"rpc\"] -> handle_rpc(req, handler_ctx, logger)"/utf8>>/binary, Js_routes/binary>>/binary, Index_route/binary>>/binary, " } } |> mist.new |> mist.port("/utf8>>/binary, (erlang:integer_to_binary(Port))/binary>>/binary, ") |> mist.start case started { Ok(_) -> process.sleep_forever() Error(reason) -> { io.println_error( \"libero: failed to start mist on port "/utf8>>/binary, (erlang:integer_to_binary(Port))/binary>>/binary, ": \" <> string.inspect(reason), ) panic as \"mist failed to start\" } } } // HTTP RPC endpoint. Accepts POST requests with ETF-encoded call envelopes, // passes them to dispatch.handle (same handler logic as WebSocket), and // returns the ETF-encoded response. State changes are not persisted across // requests since HTTP is stateless. Use WebSocket for stateful interactions. fn handle_rpc( req: Request(Connection), handler_ctx: "/utf8>>/binary, Context_qualifier/binary>>/binary, ".HandlerContext, logger: ws_logger.Logger, ) -> response.Response(mist.ResponseData) { case mist.read_body(req, 1_000_000) { Ok(req) -> { let #(response_bytes, maybe_panic, _new_handler_ctx) = dispatch.handle(handler_ctx:, data: req.body) case maybe_panic { Some(info) -> logger.error( \"RPC panic: \" <> info.fn_name <> \" (trace \" <> info.trace_id <> \"): \" <> info.reason, ) None -> Nil } response.new(200) |> response.set_header(\"content-type\", \"application/octet-stream\") |> response.set_body(mist.Bytes(bytes_tree.from_bit_array(response_bytes))) } Error(_) -> response.new(400) |> response.set_body(mist.Bytes(bytes_tree.from_string(\"Bad request\"))) } } // Return an HTML string as a response. fn serve_html(html: String) -> response.Response(mist.ResponseData) { response.new(200) |> response.set_header(\"content-type\", \"text/html\") |> response.set_body(mist.Bytes(bytes_tree.from_string(html))) } // Serve a static file from disk. Used for client JS bundles and assets. fn serve_file( path: String, ) -> response.Response(mist.ResponseData) { case mist.send_file(path, offset: 0, limit: None) { Ok(body) -> response.new(200) |> response.set_header(\"content-type\", content_type(path)) |> response.set_body(body) Error(_) -> response.new(404) |> response.set_body(mist.Bytes(bytes_tree.from_string(\"Not found\"))) } } // Map file extensions to MIME types for static file serving. fn content_type(path: String) -> String { case string.split(path, \".\") |> list.last { Ok(\"js\") | Ok(\"mjs\") -> \"application/javascript\" Ok(\"css\") -> \"text/css\" Ok(\"html\") -> \"text/html\" Ok(\"json\") -> \"application/json\" Ok(\"wasm\") -> \"application/wasm\" Ok(\"svg\") -> \"image/svg+xml\" Ok(\"png\") -> \"image/png\" Ok(\"ico\") -> \"image/x-icon\" Ok(\"map\") -> \"application/json\" _ -> \"application/octet-stream\" } } "/utf8>>, libero@codegen:ensure_parent_dir(Output), write_if_missing(Output, Content).