Current section
Files
Jump to
Current section
Files
src/dream@servers@mist@websocket.erl
-module(dream@servers@mist@websocket).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/dream/servers/mist/websocket.gleam").
-export([continue_connection/1, continue_connection_with_selector/2, stop_connection/0, upgrade_websocket/5, send_text/2, send_binary/2]).
-export_type([connection/0, message/1, send_error/0, action/2]).
-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(
" WebSocket support for Dream (Mist server adapter)\n"
"\n"
" This module provides Dream's WebSocket abstraction for the Mist server\n"
" adapter. It upgrades an HTTP request to a WebSocket connection and runs\n"
" a typed message loop driven by your handler functions.\n"
"\n"
" Most applications will import this module as:\n"
"\n"
" ```gleam\n"
" import dream/servers/mist/websocket\n"
" ```\n"
"\n"
" ## Concepts\n"
"\n"
" - `Connection` – opaque handle to the WebSocket. Use it to send messages\n"
" back to the client.\n"
" - `Message(custom)` – messages received from the client or from your\n"
" application via a `Selector(custom)` (text, binary, custom, closed).\n"
" - `Action(state, custom)` – the next step in the WebSocket state machine,\n"
" returned from your `on_message` handler.\n"
"\n"
" The typical lifecycle is:\n"
"\n"
" 1. Router sends a request to a controller.\n"
" 2. Controller calls `upgrade_websocket`.\n"
" 3. `on_init` runs once when the WebSocket is established.\n"
" 4. `on_message` runs for each incoming or custom message.\n"
" 5. `on_close` runs after the connection closes.\n"
"\n"
" Handlers follow Dream's \"no closures\" rule: instead of capturing\n"
" dependencies, you define a `Dependencies` type and pass it explicitly\n"
" into `upgrade_websocket` so every handler receives what it needs.\n"
"\n"
" ## Example (echo chat)\n"
"\n"
" ```gleam\n"
" import dream/http/request.{type Request}\n"
" import dream/http/response.{type Response, text_response}\n"
" import dream/http/status\n"
" import dream/servers/mist/websocket\n"
" import gleam/erlang/process\n"
" import gleam/option\n"
"\n"
" pub type Dependencies {\n"
" Dependencies(user_name: String)\n"
" }\n"
"\n"
" pub fn handle_upgrade(request: Request, context, services) -> Response {\n"
" let user = request.get_query_param(request.query, \"user\")\n"
" let user = option.unwrap(user, \"Anonymous\")\n"
" let deps = Dependencies(user_name: user)\n"
"\n"
" websocket.upgrade_websocket(\n"
" request,\n"
" dependencies: deps,\n"
" on_init: handle_init,\n"
" on_message: handle_message,\n"
" on_close: handle_close,\n"
" )\n"
" }\n"
"\n"
" fn handle_init(\n"
" _connection: websocket.Connection,\n"
" _deps: Dependencies,\n"
" ) -> #(String, option.Option(process.Selector(String))) {\n"
" // Initial state is the username, no extra messages yet\n"
" #(\"\", option.None)\n"
" }\n"
"\n"
" fn handle_message(\n"
" state: String,\n"
" message: websocket.Message(String),\n"
" connection: websocket.Connection,\n"
" deps: Dependencies,\n"
" ) -> websocket.Action(String, String) {\n"
" case message {\n"
" websocket.TextMessage(text) -> {\n"
" let reply = deps.user_name <> \": \" <> text\n"
" let _ = websocket.send_text(connection, reply)\n"
" websocket.continue_connection(state)\n"
" }\n"
" websocket.ConnectionClosed -> websocket.stop_connection()\n"
" _ -> websocket.continue_connection(state)\n"
" }\n"
" }\n"
"\n"
" fn handle_close(_state: String, _deps: Dependencies) -> Nil {\n"
" Nil\n"
" }\n"
" ```\n"
).
-opaque connection() :: {connection,
mist@internal@websocket:websocket_connection()}.
-type message(AHXO) :: {text_message, binary()} |
{binary_message, bitstring()} |
{custom_message, AHXO} |
connection_closed.
-type send_error() :: socket_closed | timeout | unknown_error.
-opaque action(AHXP, AHXQ) :: {action, mist:next(AHXP, AHXQ)}.
-file("src/dream/servers/mist/websocket.gleam", 238).
?DOC(" Continue the WebSocket loop with the current state.\n").
-spec continue_connection(AHXZ) -> action(AHXZ, any()).
continue_connection(State) ->
{action, mist:continue(State)}.
-file("src/dream/servers/mist/websocket.gleam", 243).
?DOC(" Continue the WebSocket loop with a selector for receiving custom messages.\n").
-spec continue_connection_with_selector(
AHYD,
gleam@erlang@process:selector(AHYE)
) -> action(AHYD, AHYE).
continue_connection_with_selector(State, Selector) ->
{action,
begin
_pipe = mist:continue(State),
mist:with_selector(_pipe, Selector)
end}.
-file("src/dream/servers/mist/websocket.gleam", 251).
?DOC(" Stop the WebSocket loop normally.\n").
-spec stop_connection() -> action(any(), any()).
stop_connection() ->
{action, mist:stop()}.
-file("src/dream/servers/mist/websocket.gleam", 297).
-spec convert_mist_message_to_dream(mist:websocket_message(AHYQ)) -> message(AHYQ).
convert_mist_message_to_dream(Mist_msg) ->
case Mist_msg of
{text, Text} ->
{text_message, Text};
{binary, Data} ->
{binary_message, Data};
{custom, Custom} ->
{custom_message, Custom};
closed ->
connection_closed;
shutdown ->
connection_closed
end.
-file("src/dream/servers/mist/websocket.gleam", 182).
?DOC(
" Upgrade an HTTP request to a WebSocket connection.\n"
"\n"
" This function must be called from within a Dream controller. It handles\n"
" the protocol upgrade and sets up the WebSocket message loop.\n"
"\n"
" ## Parameters\n"
"\n"
" * `request` - The incoming HTTP request\n"
" * `dependencies` - Application dependencies (services, context, etc.) passed to all handlers\n"
" * `on_init` - Called when the WebSocket connects. Returns initial state and optional selector.\n"
" * `on_message` - Called for each WebSocket message. Returns the next action.\n"
" * `on_close` - Called when the connection closes.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" websocket.upgrade_websocket(\n"
" request,\n"
" dependencies: services,\n"
" on_init: init_handler,\n"
" on_message: message_handler,\n"
" on_close: close_handler,\n"
" )\n"
"\n"
" fn init_handler(connection, services) {\n"
" #(initial_state, None)\n"
" }\n"
"\n"
" fn message_handler(state, message, connection, services) {\n"
" case message {\n"
" websocket.TextMessage(text) -> {\n"
" // Handle text message\n"
" websocket.continue_connection(state)\n"
" }\n"
" websocket.CustomMessage(msg) -> {\n"
" // Handle custom message from selector\n"
" websocket.continue_connection(state)\n"
" }\n"
" websocket.ConnectionClosed -> websocket.stop_connection()\n"
" _ -> websocket.continue_connection(state)\n"
" }\n"
" }\n"
"\n"
" fn close_handler(state, services) {\n"
" Nil\n"
" }\n"
" ```\n"
).
-spec upgrade_websocket(
dream@http@request:request(),
AHXR,
fun((connection(), AHXR) -> {AHXS,
gleam@option:option(gleam@erlang@process:selector(AHXT))}),
fun((AHXS, message(AHXT), connection(), AHXR) -> action(AHXS, AHXT)),
fun((AHXS, AHXR) -> nil)
) -> dream@http@response:response().
upgrade_websocket(_, Dependencies, On_init, On_message, On_close) ->
Request_key = erlang:binary_to_atom(<<"dream_mist_request"/utf8>>),
Raw_request = erlang:get(Request_key),
Mist_request = gleam_stdlib:identity(Raw_request),
Wrapped_on_init = fun(Mist_conn) ->
Dream_conn = {connection, Mist_conn},
On_init(Dream_conn, Dependencies)
end,
Wrapped_handler = fun(State, Mist_msg, Mist_conn@1) ->
Dream_conn@1 = {connection, Mist_conn@1},
Dream_msg = convert_mist_message_to_dream(Mist_msg),
{action, Mist_action} = On_message(
State,
Dream_msg,
Dream_conn@1,
Dependencies
),
Mist_action
end,
Wrapped_on_close = fun(State@1) -> On_close(State@1, Dependencies) end,
Mist_response = mist:websocket(
Mist_request,
Wrapped_handler,
Wrapped_on_init,
Wrapped_on_close
),
Response_key = erlang:binary_to_atom(<<"dream_mist_response"/utf8>>),
erlang:put(Response_key, gleam_stdlib:identity({some, Mist_response})),
dream@http@response:empty_response(200).
-file("src/dream/servers/mist/websocket.gleam", 308).
-spec convert_socket_error({ok, nil} | {error, any()}) -> {ok, nil} |
{error, send_error()}.
convert_socket_error(Result) ->
case Result of
{ok, Nil} ->
{ok, Nil};
{error, _} ->
{error, socket_closed}
end.
-file("src/dream/servers/mist/websocket.gleam", 265).
?DOC(
" Send a text frame to the client.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" case websocket.send_text(connection, \"Hello!\") {\n"
" Ok(Nil) -> // Message sent successfully\n"
" Error(reason) -> // Handle send error\n"
" }\n"
" ```\n"
).
-spec send_text(connection(), binary()) -> {ok, nil} | {error, send_error()}.
send_text(Connection, Message) ->
{connection, Mist_conn} = Connection,
_pipe = mist:send_text_frame(Mist_conn, Message),
convert_socket_error(_pipe).
-file("src/dream/servers/mist/websocket.gleam", 284).
?DOC(
" Send a binary frame to the client.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" case websocket.send_binary(connection, data) {\n"
" Ok(Nil) -> // Message sent successfully\n"
" Error(reason) -> // Handle send error\n"
" }\n"
" ```\n"
).
-spec send_binary(connection(), bitstring()) -> {ok, nil} |
{error, send_error()}.
send_binary(Connection, Message) ->
{connection, Mist_conn} = Connection,
_pipe = mist:send_binary_frame(Mist_conn, Message),
convert_socket_error(_pipe).