Packages

A small, typed, ergonomic WebSocket layer for Gleam applications built on top of Mist

Current section

Files

Jump to
wist src wist@adapters@mist.erl
Raw

src/wist@adapters@mist.erl

-module(wist@adapters@mist).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/wist/adapters/mist.gleam").
-export([upgrade/3]).
-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(
" Wist adapter for the Mist web server.\n"
"\n"
" This module implements the transport adapter bridging Mist's WebSocket upgrade\n"
" logic and connection actor loops to Wist's transport-neutral `Handler` API.\n"
"\n"
" Use `upgrade` inside your Mist HTTP request handler to switch protocols and\n"
" run a stateful Wist handler.\n"
).
-file("src/wist/adapters/mist.gleam", 212).
-spec to_gramps_close_reason(gleam@option:option(wist:close_frame())) -> gramps@websocket:close_reason().
to_gramps_close_reason(Maybe_frame) ->
case Maybe_frame of
none ->
{normal, <<>>};
{some, {close_frame, Code, Reason}} ->
{custom_close_reason, Code, gleam_stdlib:identity(Reason)}
end.
-file("src/wist/adapters/mist.gleam", 180).
-spec send_frame(mist@internal@websocket:websocket_connection(), wist:frame()) -> {ok,
nil} |
{error, nil}.
send_frame(Connection, Frame) ->
case Frame of
{text, Text} ->
_pipe = mist:send_text_frame(Connection, Text),
gleam@result:replace_error(_pipe, nil);
{binary, Bin} ->
_pipe@1 = mist:send_binary_frame(Connection, Bin),
gleam@result:replace_error(_pipe@1, nil);
{ping, Data} ->
Bytes = gramps@websocket:encode_ping_frame(Data, none),
_pipe@2 = glisten@transport:send(
erlang:element(3, Connection),
erlang:element(2, Connection),
Bytes
),
gleam@result:replace_error(_pipe@2, nil);
{pong, Data@1} ->
Bytes@1 = gramps@websocket:encode_pong_frame(Data@1, none),
_pipe@3 = glisten@transport:send(
erlang:element(3, Connection),
erlang:element(2, Connection),
Bytes@1
),
gleam@result:replace_error(_pipe@3, nil);
{close, Maybe_close} ->
Reason = to_gramps_close_reason(Maybe_close),
Bytes@2 = gramps@websocket:encode_close_frame(Reason, none),
_pipe@4 = glisten@transport:send(
erlang:element(3, Connection),
erlang:element(2, Connection),
Bytes@2
),
gleam@result:replace_error(_pipe@4, nil)
end.
-file("src/wist/adapters/mist.gleam", 151).
-spec run_effects(
list(wist:effect(NDB)),
mist@internal@websocket:websocket_connection(),
wist:codec(any(), NDB)
) -> {boolean(), list(wist:effect(NDB))}.
run_effects(Effects, Connection, Codec) ->
case Effects of
[] ->
{false, []};
[{close_connection, Maybe_close} | _] ->
_ = send_frame(Connection, {close, Maybe_close}),
{true, []};
[{send, Msg} | Rest] ->
case (erlang:element(3, Codec))(Msg) of
{ok, Frame} ->
_ = send_frame(Connection, Frame),
run_effects(Rest, Connection, Codec);
{error, _} ->
{true, []}
end;
[{send_frame, Frame@1} | Rest@1] ->
_ = send_frame(Connection, Frame@1),
run_effects(Rest@1, Connection, Codec)
end.
-file("src/wist/adapters/mist.gleam", 123).
-spec process_inbound(
NCR,
wist:frame(),
wist:handler(NCR, NCS, NCT),
wist:codec(NCS, NCT),
mist@internal@websocket:websocket_connection()
) -> mist:next(NCR, nil).
process_inbound(State, Frame, Handler, Codec, Connection) ->
Update_fn = wist:get_update(Handler),
case (erlang:element(2, Codec))(Frame) of
{ok, Decoded} ->
{Next_state, Effects} = Update_fn(State, {message, Decoded}),
{Should_close, _} = run_effects(Effects, Connection, Codec),
case Should_close of
true ->
mist:stop();
false ->
mist:continue(Next_state)
end;
{error, _} ->
_ = Update_fn(
State,
{failed, {socket_error, <<"Frame decode failed"/utf8>>}}
),
mist:stop()
end.
-file("src/wist/adapters/mist.gleam", 53).
?DOC(
" Upgrades an incoming HTTP request to a WebSocket connection managed by Wist.\n"
"\n"
" If the request is a valid WebSocket handshake request, it returns a 101 Switching\n"
" Protocols response body configured to spawn Mist's WebSocket handler process.\n"
" If the request is malformed or invalid, a 400 Bad Request response is returned.\n"
"\n"
" ### Parameters\n"
" - `request`: The incoming HTTP request carrying the Mist transport `Connection`.\n"
" - `handler`: The stateful `wist.Handler` defining initial state and event transitions.\n"
" - `codec`: A `wist.Codec` specifying how to encode and decode wire frames.\n"
"\n"
" ### Guarantees\n"
" - **Serialization**: Connection events are processed strictly sequentially. `update` is never run concurrently.\n"
" - **FIFO Effects**: Effects returned by `update` are executed in the exact order they are listed.\n"
" - **Safety**: If frame decoding fails, the connection is closed immediately, and a `Failed` event is dispatched.\n"
"\n"
" ### Limitations\n"
" - Outbound pings are supported via the `wist.Ping` effect, but client `Pong` frames are swallowed by Mist's internal wrapper and will not be dispatched as events.\n"
"\n"
" ### Example\n"
" ```gleam\n"
" import wist\n"
" import wist/adapters/mist as wist_mist\n"
"\n"
" fn my_http_handler(req) {\n"
" case req.path {\n"
" \"/ws\" -> wist_mist.upgrade(req, my_ws_handler, wist.raw_codec())\n"
" _ -> response.new(200)\n"
" }\n"
" }\n"
" ```\n"
).
-spec upgrade(
gleam@http@request:request(mist@internal@http:connection()),
wist:handler(any(), NCJ, NCK),
wist:codec(NCJ, NCK)
) -> gleam@http@response:response(mist:response_data()).
upgrade(Request, Handler, Codec) ->
Context = {context,
erlang:element(8, Request),
erlang:element(3, Request),
erlang:element(9, Request)},
On_init = fun(Connection) ->
Init_state_fn = wist:get_init_state(Handler),
Update_fn = wist:get_update(Handler),
User_state = Init_state_fn(Context),
{User_state@1, Opened_effects} = Update_fn(User_state, opened),
{Should_close, _} = run_effects(Opened_effects, Connection, Codec),
case Should_close of
true ->
_ = glisten@transport:close(
erlang:element(3, Connection),
erlang:element(2, Connection)
),
nil;
false ->
nil
end,
{User_state@1, none}
end,
Mist_handler = fun(State, Message, Connection@1) -> case Message of
{text, Text} ->
Frame = {text, Text},
process_inbound(State, Frame, Handler, Codec, Connection@1);
{binary, Bin} ->
Frame@1 = {binary, Bin},
process_inbound(State, Frame@1, Handler, Codec, Connection@1);
_ ->
mist:continue(State)
end end,
On_close = fun(State@1) ->
Update_fn@1 = wist:get_update(Handler),
_ = Update_fn@1(State@1, {closed, unknown}),
nil
end,
mist:websocket(Request, Mist_handler, On_init, On_close).